Skip to content

Commit

Permalink
Custom encoders (#9)
Browse files Browse the repository at this point in the history
* Custom encoder support

* Use json no msgpack
  • Loading branch information
jackkleeman authored Jul 15, 2024
1 parent 5924d37 commit cc06d6d
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 85 deletions.
70 changes: 70 additions & 0 deletions encoding/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package encoding

import (
"encoding/json"

"google.golang.org/protobuf/proto"
)

type InputPayload struct {
Required bool `json:"required"`
ContentType *string `json:"contentType,omitempty"`
JsonSchema interface{} `json:"jsonSchema,omitempty"`
}

type OutputPayload struct {
ContentType *string `json:"contentType,omitempty"`
SetContentTypeIfEmpty bool `json:"setContentTypeIfEmpty"`
JsonSchema interface{} `json:"jsonSchema,omitempty"`
}

type JSONDecoder[I any] struct{}

func (j JSONDecoder[I]) InputPayload() *InputPayload {
return &InputPayload{Required: true, ContentType: proto.String("application/json")}
}

func (j JSONDecoder[I]) Decode(data []byte) (input I, err error) {
err = json.Unmarshal(data, &input)
return
}

type JSONEncoder[O any] struct{}

func (j JSONEncoder[O]) OutputPayload() *OutputPayload {
return &OutputPayload{ContentType: proto.String("application/json")}
}

func (j JSONEncoder[O]) Encode(output O) ([]byte, error) {
return json.Marshal(output)
}

type MessagePointer[I any] interface {
proto.Message
*I
}

type ProtoDecoder[I any, IP MessagePointer[I]] struct{}

func (p ProtoDecoder[I, IP]) InputPayload() *InputPayload {
return &InputPayload{Required: true, ContentType: proto.String("application/proto")}
}

func (p ProtoDecoder[I, IP]) Decode(data []byte) (input IP, err error) {
// Unmarshal expects a non-nil pointer to a proto.Message implementing struct
// hence we must have a type parameter for the struct itself (I) and here we allocate
// a non-nil pointer of type IP
input = IP(new(I))
err = proto.Unmarshal(data, input)
return
}

type ProtoEncoder[O proto.Message] struct{}

func (p ProtoEncoder[O]) OutputPayload() *OutputPayload {
return &OutputPayload{ContentType: proto.String("application/proto")}
}

func (p ProtoEncoder[O]) Encode(output O) ([]byte, error) {
return proto.Marshal(output)
}
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ require (
github.com/mr-tron/base58 v1.2.0
github.com/posener/h2conn v0.0.0-20231204025407-3997deeca0f0
github.com/stretchr/testify v1.9.0
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/net v0.21.0
google.golang.org/protobuf v1.32.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ github.com/posener/h2conn v0.0.0-20231204025407-3997deeca0f0 h1:zZg03nifrj6ayWNa
github.com/posener/h2conn v0.0.0-20231204025407-3997deeca0f0/go.mod h1:bblJa8QcHntareAJYfLJUzLj42sUFBKCBeTDK5LyUrw=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
Expand Down
79 changes: 61 additions & 18 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,102 @@ package restate
import (
"encoding/json"
"fmt"

"github.com/restatedev/sdk-go/encoding"
)

// Void is a placeholder used usually for functions that their signature require that
// you accept an input or return an output but the function implementation does not
// require them
type Void struct{}

func (v Void) MarshalJSON() ([]byte, error) {
return []byte("null"), nil
type VoidDecoder struct{}

func (v VoidDecoder) InputPayload() *encoding.InputPayload {
return &encoding.InputPayload{}
}

func (v VoidDecoder) Decode(data []byte) (input Void, err error) {
if len(data) > 0 {
err = fmt.Errorf("restate.Void decoder expects no request data")
}
return
}

type VoidEncoder struct{}

func (v VoidEncoder) OutputPayload() *encoding.OutputPayload {
return &encoding.OutputPayload{}
}

func (v *Void) UnmarshalJSON(_ []byte) error {
return nil
func (v VoidEncoder) Encode(output Void) ([]byte, error) {
return nil, nil
}

type serviceHandler[I any, O any] struct {
fn ServiceHandlerFn[I, O]
fn ServiceHandlerFn[I, O]
decoder Decoder[I]
encoder Encoder[O]
}

// NewServiceHandler create a new handler for a service
func NewServiceHandler[I any, O any](fn ServiceHandlerFn[I, O]) *serviceHandler[I, O] {
// NewJSONServiceHandler create a new handler for a service using JSON encoding
func NewJSONServiceHandler[I any, O any](fn ServiceHandlerFn[I, O]) *serviceHandler[I, O] {
return &serviceHandler[I, O]{
fn: fn,
fn: fn,
decoder: encoding.JSONDecoder[I]{},
encoder: encoding.JSONEncoder[O]{},
}
}

func (h *serviceHandler[I, O]) Call(ctx Context, bytes []byte) ([]byte, error) {
input := new(I)
// NewProtoServiceHandler create a new handler for a service using protobuf encoding
// Input and output type must both be pointers that satisfy proto.Message
func NewProtoServiceHandler[I any, O any, IP encoding.MessagePointer[I], OP encoding.MessagePointer[O]](fn ServiceHandlerFn[IP, OP]) *serviceHandler[IP, OP] {
return &serviceHandler[IP, OP]{
fn: fn,
decoder: encoding.ProtoDecoder[I, IP]{},
encoder: encoding.ProtoEncoder[OP]{},
}
}

if len(bytes) > 0 {
// use the zero value if there is no input data at all
if err := json.Unmarshal(bytes, input); err != nil {
return nil, TerminalError(fmt.Errorf("request doesn't match handler signature: %w", err))
}
// NewServiceHandlerWithEncoders create a new handler for a service using a custom encoder/decoder implementation
func NewServiceHandlerWithEncoders[I any, O any](fn ServiceHandlerFn[I, O], decoder Decoder[I], encoder Encoder[O]) *serviceHandler[I, O] {
return &serviceHandler[I, O]{
fn: fn,
decoder: decoder,
encoder: encoder,
}
}

func (h *serviceHandler[I, O]) Call(ctx Context, bytes []byte) ([]byte, error) {
input, err := h.decoder.Decode(bytes)
if err != nil {
return nil, TerminalError(fmt.Errorf("request could not be decoded into handler input type: %w", err))
}

// we are sure about the fn signature so it's safe to do this
output, err := h.fn(
ctx,
*input,
input,
)
if err != nil {
return nil, err
}

bytes, err = json.Marshal(output)
bytes, err = h.encoder.Encode(output)
if err != nil {
return nil, TerminalError(fmt.Errorf("failed to serialize output: %w", err))
}

return bytes, nil
}

func (h *serviceHandler[I, O]) InputPayload() *encoding.InputPayload {
return h.decoder.InputPayload()
}

func (h *serviceHandler[I, O]) OutputPayload() *encoding.OutputPayload {
return h.encoder.OutputPayload()
}

func (h *serviceHandler[I, O]) sealed() {}

type objectHandler[I any, O any] struct {
Expand Down
20 changes: 5 additions & 15 deletions internal/discovery.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package internal

import "github.com/restatedev/sdk-go/encoding"

type ProtocolMode string

const (
Expand All @@ -23,24 +25,12 @@ const (
ServiceHandlerType_SHARED ServiceHandlerType = "SHARED"
)

type InputPayload struct {
Required bool `json:"required"`
ContentType string `json:"contentType"`
JsonSchema interface{} `json:"jsonSchema,omitempty"`
}

type OutputPayload struct {
ContentType string `json:"contentType"`
SetContentTypeIfEmpty bool `json:"setContentTypeIfEmpty"`
JsonSchema interface{} `json:"jsonSchema,omitempty"`
}

type Handler struct {
Name string `json:"name,omitempty"`
// If unspecified, defaults to EXCLUSIVE for Virtual Object. This should be unset for Services.
Ty *ServiceHandlerType `json:"ty,omitempty"`
Input *InputPayload `json:"input,omitempty"`
Output *OutputPayload `json:"output,omitempty"`
Ty *ServiceHandlerType `json:"ty,omitempty"`
Input *encoding.InputPayload `json:"input,omitempty"`
Output *encoding.OutputPayload `json:"output,omitempty"`
}

type Service struct {
Expand Down
Loading

0 comments on commit cc06d6d

Please sign in to comment.