Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom encoders #9

Merged
merged 2 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading