From 485e256613d91f5d3e78f961d06d0cf9303f337a Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 31 Jan 2024 16:45:04 -0700 Subject: [PATCH] feat: add support for HttpRequest[[]byte] (#860) Fixes #849 Fixes #850 Fixes #856 --- backend/controller/controller.go | 2 +- backend/controller/ingress/alias.go | 57 +++++++ backend/controller/ingress/ingress.go | 169 ++++++++++++--------- backend/controller/ingress/ingress_test.go | 55 +++++-- backend/schema/jsonschema.go | 13 +- backend/schema/schema.go | 13 ++ examples/go/httpingress/go.mod | 41 +---- examples/go/httpingress/go.sum | 147 ------------------ examples/go/httpingress/httpingress.go | 70 ++++----- go-runtime/encoding/encoding.go | 2 - go-runtime/server/server.go | 3 +- 11 files changed, 255 insertions(+), 317 deletions(-) create mode 100644 backend/controller/ingress/alias.go diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 4e4dbfe95e..f83c06f896 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -249,7 +249,7 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { } ingress.SetDefaultContentType(response.Headers) - responseBody, err = ingress.ResponseBodyForContentType(response.Headers, response.Body) + responseBody, err = ingress.ResponseBodyForVerb(sch, verb, response.Body, response.Headers) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/backend/controller/ingress/alias.go b/backend/controller/ingress/alias.go new file mode 100644 index 0000000000..b10b3aafd0 --- /dev/null +++ b/backend/controller/ingress/alias.go @@ -0,0 +1,57 @@ +package ingress + +import ( + "github.com/TBD54566975/ftl/backend/schema" +) + +func transformFromAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) { + data, err := sch.ResolveDataRefMonomorphised(dataRef) + if err != nil { + return nil, err + } + + for _, field := range data.Fields { + if _, ok := request[field.Name]; !ok && field.Alias != "" && request[field.Alias] != nil { + request[field.Name] = request[field.Alias] + delete(request, field.Alias) + } + + if d, ok := field.Type.(*schema.DataRef); ok { + if _, found := request[field.Name]; found { + rMap, err := transformFromAliasedFields(d, sch, request[field.Name].(map[string]any)) + if err != nil { + return nil, err + } + request[field.Name] = rMap + } + } + } + + return request, nil +} + +func transformToAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) { + data, err := sch.ResolveDataRefMonomorphised(dataRef) + if err != nil { + return nil, err + } + + for _, field := range data.Fields { + if field.Alias != "" && field.Name != field.Alias { + request[field.Alias] = request[field.Name] + delete(request, field.Name) + } + + if d, ok := field.Type.(*schema.DataRef); ok { + if _, found := request[field.Name]; found { + rMap, err := transformToAliasedFields(d, sch, request[field.Name].(map[string]any)) + if err != nil { + return nil, err + } + request[field.Name] = rMap + } + } + } + + return request, nil +} diff --git a/backend/controller/ingress/ingress.go b/backend/controller/ingress/ingress.go index 19cedd00c4..262e48171d 100644 --- a/backend/controller/ingress/ingress.go +++ b/backend/controller/ingress/ingress.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "math/rand" "net/http" "net/url" @@ -67,20 +68,6 @@ func SetDefaultContentType(headers map[string][]string) { } } -func ResponseBodyForContentType(headers map[string][]string, body []byte) ([]byte, error) { - if contentType, hasContentType := headers["Content-Type"]; hasContentType { - if strings.HasPrefix(contentType[0], "text/") { - var textContent string - if err := json.Unmarshal(body, &textContent); err != nil { - return nil, err - } - return []byte(textContent), nil - } - } - - return body, nil -} - func ValidateCallBody(body []byte, verbRef *schema.VerbRef, sch *schema.Schema) error { verb := sch.ResolveVerbRef(verbRef) if verb == nil { @@ -116,7 +103,7 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch return nil, fmt.Errorf("verb %s input must be a data structure", verb.Name) } - bodyMap, err := buildRequest(route, r, request, sch) + httpRequestBody, err := extractHTTPRequestBody(route, r, request, sch) if err != nil { return nil, err } @@ -127,9 +114,9 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch requestMap["pathParameters"] = pathParameters requestMap["query"] = r.URL.Query() requestMap["headers"] = r.Header - requestMap["body"] = bodyMap + requestMap["body"] = httpRequestBody - requestMap, err = transformAliasedFields(request, sch, requestMap) + requestMap, err = transformFromAliasedFields(request, sch, requestMap) if err != nil { return nil, err } @@ -149,7 +136,7 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch return nil, fmt.Errorf("verb %s input must be a data structure", verb.Name) } - requestMap, err := buildRequest(route, r, request, sch) + requestMap, err := buildRequestMap(route, r, request, sch) if err != nil { return nil, err } @@ -168,7 +155,93 @@ func ValidateAndExtractRequestBody(route *dal.IngressRoute, r *http.Request, sch return body, nil } -func buildRequest(route *dal.IngressRoute, r *http.Request, dataRef *schema.DataRef, sch *schema.Schema) (map[string]any, error) { +func ResponseBodyForVerb(sch *schema.Schema, verb *schema.Verb, body []byte, headers map[string][]string) ([]byte, error) { + if contentType, hasContentType := headers["Content-Type"]; hasContentType { + if strings.HasPrefix(contentType[0], "text/") { + var textContent string + if err := json.Unmarshal(body, &textContent); err != nil { + return nil, err + } + return []byte(textContent), nil + } + } + + responseRef, ok := verb.Response.(*schema.DataRef) + if !ok { + return body, nil + } + + bodyField, err := getBodyField(responseRef, sch) + if err != nil { + return nil, err + } + + if bodyType, ok := bodyField.Type.(*schema.DataRef); ok { + var responseMap map[string]any + err := json.Unmarshal(body, &responseMap) + if err != nil { + return nil, fmt.Errorf("HTTP response body is not valid JSON: %w", err) + } + + aliasedResponseMap, err := transformToAliasedFields(bodyType, sch, responseMap) + if err != nil { + return nil, err + } + + return json.Marshal(aliasedResponseMap) + } + + return body, nil +} + +func getBodyField(dataRef *schema.DataRef, sch *schema.Schema) (*schema.Field, error) { + data, err := sch.ResolveDataRefMonomorphised(dataRef) + if err != nil { + return nil, err + } + var bodyField *schema.Field + for _, field := range data.Fields { + if field.Name == "body" { + bodyField = field + break + } + } + + if bodyField == nil { + return nil, fmt.Errorf("verb %s must have a 'body' field", dataRef.Name) + } + + return bodyField, nil +} + +func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, dataRef *schema.DataRef, sch *schema.Schema) (any, error) { + bodyField, err := getBodyField(dataRef, sch) + if err != nil { + return nil, err + } + + switch bodyType := bodyField.Type.(type) { + case *schema.DataRef: + bodyMap, err := buildRequestMap(route, r, bodyType, sch) + if err != nil { + return nil, err + } + return bodyMap, nil + + case *schema.Bytes: + defer r.Body.Close() + bodyData, err := io.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("error reading request body: %w", err) + } + return bodyData, nil + + default: + return nil, fmt.Errorf("unsupported HttpRequest.Body type %T", bodyField.Type) + } +} + +func buildRequestMap(route *dal.IngressRoute, r *http.Request, dataRef *schema.DataRef, sch *schema.Schema) (map[string]any, error) { requestMap := map[string]any{} matchSegments(route.Path, r.URL.Path, func(segment, value string) { requestMap[segment] = value @@ -187,17 +260,9 @@ func buildRequest(route *dal.IngressRoute, r *http.Request, dataRef *schema.Data requestMap[k] = v } default: - data := sch.ResolveDataRef(dataRef) - if data == nil { - return nil, fmt.Errorf("unknown data %v", dataRef) - } - - if len(dataRef.TypeParameters) > 0 { - var err error - data, err = data.Monomorphise(dataRef.TypeParameters...) - if err != nil { - return nil, err - } + data, err := sch.ResolveDataRefMonomorphised(dataRef) + if err != nil { + return nil, err } queryMap, err := parseQueryParams(r.URL.Query(), data) @@ -214,17 +279,9 @@ func buildRequest(route *dal.IngressRoute, r *http.Request, dataRef *schema.Data } func validateRequestMap(dataRef *schema.DataRef, path path, request map[string]any, sch *schema.Schema) error { - data := sch.ResolveDataRef(dataRef) - if data == nil { - return fmt.Errorf("unknown data %v", dataRef) - } - - if len(dataRef.TypeParameters) > 0 { - var err error - data, err = data.Monomorphise(dataRef.TypeParameters...) - if err != nil { - return err - } + data, err := sch.ResolveDataRefMonomorphised(dataRef) + if err != nil { + return err } var errs []error @@ -462,33 +519,3 @@ func decodeQueryJSON(query string) (map[string]any, error) { func hasInvalidQueryChars(s string) bool { return strings.ContainsAny(s, "{}[]|\\^`") } - -func transformAliasedFields(dataRef *schema.DataRef, sch *schema.Schema, request map[string]any) (map[string]any, error) { - data := sch.ResolveDataRef(dataRef) - if len(dataRef.TypeParameters) > 0 { - var err error - data, err = data.Monomorphise(dataRef.TypeParameters...) - if err != nil { - return nil, err - } - } - - for _, field := range data.Fields { - if _, ok := request[field.Name]; !ok && field.Alias != "" && request[field.Alias] != nil { - request[field.Name] = request[field.Alias] - delete(request, field.Alias) - } - - if d, ok := field.Type.(*schema.DataRef); ok { - if _, found := request[field.Name]; found { - rMap, err := transformAliasedFields(d, sch, request[field.Name].(map[string]any)) - if err != nil { - return nil, err - } - request[field.Name] = rMap - } - } - } - - return request, nil -} diff --git a/backend/controller/ingress/ingress_test.go b/backend/controller/ingress/ingress_test.go index a7efe487f2..b7a7117c99 100644 --- a/backend/controller/ingress/ingress_test.go +++ b/backend/controller/ingress/ingress_test.go @@ -151,36 +151,73 @@ func TestSetDefaultContentType(t *testing.T) { assert.Equal(t, map[string][]string{"Content-Type": {"text/html"}}, headers) } -func TestResponseBodyForContentType(t *testing.T) { +func TestResponseBodyForVerb(t *testing.T) { + jsonVerb := &schema.Verb{ + Name: "Json", + Response: &schema.DataRef{Module: "builtin", Name: "HttpResponse", TypeParameters: []schema.Type{ + &schema.DataRef{ + Module: "test", + Name: "Test", + TypeParameters: []schema.Type{}, + }, + }}, + } + bytesVerb := &schema.Verb{ + Name: "Json", + Response: &schema.DataRef{Module: "builtin", Name: "HttpResponse", TypeParameters: []schema.Type{ + &schema.Bytes{}, + }}, + } + sch := &schema.Schema{ + Modules: []*schema.Module{ + schema.Builtins(), + { + Name: "test", + Decls: []schema.Decl{ + &schema.Data{ + Name: "Test", + Fields: []*schema.Field{ + {Name: "message", Type: &schema.String{}, Alias: "msg"}, + }, + }, + jsonVerb, + }, + }, + }, + } tests := []struct { name string + verb *schema.Verb headers map[string][]string body []byte expectedBody []byte }{ { name: "application/json", + verb: jsonVerb, headers: map[string][]string{"Content-Type": {"application/json"}}, body: []byte(`{"message": "Hello, World!"}`), - expectedBody: []byte(`{"message": "Hello, World!"}`), + expectedBody: []byte(`{"msg":"Hello, World!"}`), + }, + { + name: "Default to application/json", + verb: jsonVerb, + headers: map[string][]string{}, + body: []byte(`{"message": "Default to JSON"}`), + expectedBody: []byte(`{"msg":"Default to JSON"}`), }, { name: "text/html", + verb: bytesVerb, headers: map[string][]string{"Content-Type": {"text/html"}}, body: []byte(`"Hello, World!"`), expectedBody: []byte("Hello, World!"), }, - { - name: "Default to application/json", - headers: map[string][]string{}, - body: []byte(`{"message": "Default to JSON"}`), - expectedBody: []byte(`{"message": "Default to JSON"}`), - }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - result, err := ResponseBodyForContentType(tc.headers, tc.body) + result, err := ResponseBodyForVerb(sch, tc.verb, tc.body, tc.headers) assert.NoError(t, err) assert.Equal(t, tc.expectedBody, result) }) diff --git a/backend/schema/jsonschema.go b/backend/schema/jsonschema.go index 59133a6674..1b9d199195 100644 --- a/backend/schema/jsonschema.go +++ b/backend/schema/jsonschema.go @@ -11,19 +11,14 @@ import ( // // It takes in the full schema in order to resolve and define references. func DataToJSONSchema(schema *Schema, dataRef DataRef) (*jsonschema.Schema, error) { - data := schema.ResolveDataRef(&dataRef) + data, err := schema.ResolveDataRefMonomorphised(&dataRef) + if err != nil { + return nil, err + } if data == nil { return nil, fmt.Errorf("unknown data type %s", dataRef) } - if len(dataRef.TypeParameters) > 0 { - var err error - data, err = data.Monomorphise(dataRef.TypeParameters...) - if err != nil { - return nil, err - } - } - // Collect all data types. dataTypes := schema.DataMap() diff --git a/backend/schema/schema.go b/backend/schema/schema.go index 94d6f1133c..c704549a72 100644 --- a/backend/schema/schema.go +++ b/backend/schema/schema.go @@ -57,6 +57,19 @@ func (s *Schema) ResolveDataRef(ref *DataRef) *Data { return nil } +func (s *Schema) ResolveDataRefMonomorphised(ref *DataRef) (*Data, error) { + data := s.ResolveDataRef(ref) + if data == nil { + return nil, fmt.Errorf("unknown data %v", ref) + } + + if len(ref.TypeParameters) > 0 { + return data.Monomorphise(ref.TypeParameters...) + } + + return data, nil +} + func (s *Schema) ResolveVerbRef(ref *VerbRef) *Verb { for _, module := range s.Modules { if module.Name == ref.Module { diff --git a/examples/go/httpingress/go.mod b/examples/go/httpingress/go.mod index e3fce197c1..5b44478e7c 100644 --- a/examples/go/httpingress/go.mod +++ b/examples/go/httpingress/go.mod @@ -2,43 +2,4 @@ module ftl/httpingress go 1.21.6 -require github.com/TBD54566975/ftl v0.105.1 - -require ( - connectrpc.com/connect v1.14.0 // indirect - connectrpc.com/grpcreflect v1.2.0 // indirect - connectrpc.com/otelconnect v0.7.0 // indirect - github.com/alecthomas/concurrency v0.0.2 // indirect - github.com/alecthomas/participle/v2 v2.1.1 // indirect - github.com/alecthomas/types v0.9.0 // indirect - github.com/alessio/shellescape v1.4.2 // indirect - github.com/danieljoos/wincred v1.2.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/google/uuid v1.5.0 // indirect - github.com/iancoleman/strcase v0.2.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.5.2 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/jpillora/backoff v1.0.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect - github.com/swaggest/jsonschema-go v0.3.64 // indirect - github.com/swaggest/refl v1.3.0 // indirect - github.com/zalando/go-keyring v0.2.3 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect - golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.20.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect -) +replace github.com/TBD54566975/ftl => ../../.. diff --git a/examples/go/httpingress/go.sum b/examples/go/httpingress/go.sum index 663a29efd1..e69de29bb2 100644 --- a/examples/go/httpingress/go.sum +++ b/examples/go/httpingress/go.sum @@ -1,147 +0,0 @@ -connectrpc.com/connect v1.14.0 h1:PDS+J7uoz5Oui2VEOMcfz6Qft7opQM9hPiKvtGC01pA= -connectrpc.com/connect v1.14.0/go.mod h1:uoAq5bmhhn43TwhaKdGKN/bZcGtzPW1v+ngDTn5u+8s= -connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= -connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= -connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= -connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= -github.com/TBD54566975/ftl v0.105.1 h1:5whQHJeqxOYlPGS7kSeY0IxNjiznZUjGvDGYrLI3Jq4= -github.com/TBD54566975/ftl v0.105.1/go.mod h1:7yRFD+2zC5zstcXYcSJzAubRi6MecQZndMnEbd1zMwM= -github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w= -github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= -github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= -github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= -github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= -github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= -github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/types v0.9.0 h1:g/P/fNElr7Ot1tWxSEms/I2gnRemxAqGNeetW6HOlO4= -github.com/alecthomas/types v0.9.0/go.mod h1:t7PnU03TVweFpbPVKaeLtFykjJD8rqiBJ7gfkp6UvLQ= -github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= -github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ= -github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= -github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= -github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= -github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= -github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= -github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA= -github.com/jackc/pgx/v5 v5.5.2/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= -github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= -github.com/swaggest/jsonschema-go v0.3.64 h1:HyB41fkA4XP0BZkqWfGap5i2JtRHQGXG/21dGDPbyLM= -github.com/swaggest/jsonschema-go v0.3.64/go.mod h1:DYuKqdpms/edvywsX6p1zHXCZkdwB28wRaBdFCe3Duw= -github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= -github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= -github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= -go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b h1:lkOPTy76R9NZ6FeDQWkDj3NsLtD8Csc9AAFYEl3kiME= -golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b/go.mod h1:QXG482h3unP32W/YwIPOc+09bvY447B7T+iLjC/JPcA= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= -golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= -modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= -modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= -modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/examples/go/httpingress/httpingress.go b/examples/go/httpingress/httpingress.go index 7c08412fe8..b55a31f83d 100644 --- a/examples/go/httpingress/httpingress.go +++ b/examples/go/httpingress/httpingress.go @@ -6,28 +6,22 @@ import ( "fmt" "ftl/builtin" - - ftl "github.com/TBD54566975/ftl/go-runtime/sdk" ) type GetRequest struct { - UserID string `json:"userId"` - PostID string `json:"postId"` + UserID string `alias:"userId"` + PostID string `alias:"postId"` } type GetResponse struct { - Message string `json:"message"` + Message string `alias:"random"` } +// Example: curl -i http://localhost:8892/ingress/http/users/123/posts?postId=456 +// //ftl:verb -//ftl:ingress http GET /http/users/{userID}/posts/{postID} +//ftl:ingress http GET /http/users/{userID}/posts func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse], error) { - logger := ftl.LoggerFromContext(ctx) - logger.Infof("Path: %s", req.Path) - logger.Infof("Method: %s", req.Method) - logger.Infof("Query: %s", req.Query) - logger.Infof("Body: %s", req.Body) - logger.Infof("Headers: %s", req.Headers) return builtin.HttpResponse[GetResponse]{ Status: 200, Headers: map[string][]string{"Get": {"Header from FTL"}}, @@ -36,44 +30,38 @@ func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.Http } type PostRequest struct { - UserID string `json:"userId"` - PostID string `json:"postId"` + UserID int `alias:"user_id"` + PostID int `alias:"post_id"` } -type PostResponse struct{} +type PostResponse struct { + Success bool `alias:"success"` +} +// Example: curl -i --json '{"user_id": 123, "post_id": 345}' http://localhost:8892/ingress/http/users +// //ftl:verb //ftl:ingress http POST /http/users func Post(ctx context.Context, req builtin.HttpRequest[PostRequest]) (builtin.HttpResponse[PostResponse], error) { - logger := ftl.LoggerFromContext(ctx) - logger.Infof("Path: %s", req.Path) - logger.Infof("Method: %s", req.Method) - logger.Infof("Query: %s", req.Query) - logger.Infof("Body: %s", req.Body) - logger.Infof("Headers: %s", req.Headers) return builtin.HttpResponse[PostResponse]{ Status: 201, Headers: map[string][]string{"Post": {"Header from FTL"}}, - Body: PostResponse{}, + Body: PostResponse{Success: true}, }, nil } type PutRequest struct { - UserID string `json:"userId"` - PostID string `json:"postId"` + UserID string `alias:"userId"` + PostID string `alias:"postId"` } type PutResponse struct{} +// Example: curl -X PUT http://localhost:8892/ingress/http/users/123 -d '{"postID": "123"}' +// //ftl:verb //ftl:ingress http PUT /http/users/{userID} func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.HttpResponse[PutResponse], error) { - logger := ftl.LoggerFromContext(ctx) - logger.Infof("Path: %s", req.Path) - logger.Infof("Method: %s", req.Method) - logger.Infof("Query: %s", req.Query) - logger.Infof("Body: %s", req.Body) - logger.Infof("Headers: %s", req.Headers) return builtin.HttpResponse[PutResponse]{ Status: 200, Headers: map[string][]string{"Put": {"Header from FTL"}}, @@ -82,20 +70,16 @@ func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.Http } type DeleteRequest struct { - UserID string `json:"userId"` + UserID string `alias:"userId"` } type DeleteResponse struct{} +// Example: curl -X DELETE http://localhost:8892/ingress/http/users/123 +// //ftl:verb //ftl:ingress http DELETE /http/users/{userID} func Delete(ctx context.Context, req builtin.HttpRequest[DeleteRequest]) (builtin.HttpResponse[DeleteResponse], error) { - logger := ftl.LoggerFromContext(ctx) - logger.Infof("Path: %s", req.Path) - logger.Infof("Method: %s", req.Method) - logger.Infof("Query: %s", req.Query) - logger.Infof("Body: %s", req.Body) - logger.Infof("Headers: %s", req.Headers) return builtin.HttpResponse[DeleteResponse]{ Status: 200, Headers: map[string][]string{"Put": {"Header from FTL"}}, @@ -114,3 +98,15 @@ func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.Ht Body: "

HTML Page From FTL 🚀!

", }, nil } + +// Example: curl -X POST http://localhost:8892/ingress/http/bytes -d 'Your data here' +// +//ftl:verb +//ftl:ingress http POST /http/bytes +func Bytes(ctx context.Context, req builtin.HttpRequest[[]byte]) (builtin.HttpResponse[[]byte], error) { + return builtin.HttpResponse[[]byte]{ + Status: 200, + Headers: map[string][]string{"Content-Type": {"application/octet-stream"}}, + Body: req.Body, + }, nil +} diff --git a/go-runtime/encoding/encoding.go b/go-runtime/encoding/encoding.go index 46403ce602..401b532599 100644 --- a/go-runtime/encoding/encoding.go +++ b/go-runtime/encoding/encoding.go @@ -19,8 +19,6 @@ var ( ) func Marshal(v any) ([]byte, error) { - data, _ := json.Marshal(v) - fmt.Println(string(data)) w := &bytes.Buffer{} err := encodeValue(reflect.ValueOf(v), w) return w.Bytes(), err diff --git a/go-runtime/server/server.go b/go-runtime/server/server.go index 67b8d380c9..1b52643dd1 100644 --- a/go-runtime/server/server.go +++ b/go-runtime/server/server.go @@ -14,6 +14,7 @@ import ( "github.com/TBD54566975/ftl/backend/common/observability" "github.com/TBD54566975/ftl/backend/common/plugin" "github.com/TBD54566975/ftl/backend/common/rpc" + "github.com/TBD54566975/ftl/go-runtime/encoding" sdkgo "github.com/TBD54566975/ftl/go-runtime/sdk" ftlv1 "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" @@ -66,7 +67,7 @@ func Handle[Req, Resp any](verb func(ctx context.Context, req Req) (Resp, error) return nil, fmt.Errorf("call to verb %s failed: %w", ref, err) } - respdata, err := json.Marshal(resp) + respdata, err := encoding.Marshal(resp) if err != nil { return nil, err }