From 2df05f692a4278cfbc11fdb676436aed702f9988 Mon Sep 17 00:00:00 2001 From: ivmorski Date: Fri, 9 Oct 2026 01:47:54 +0200 Subject: [PATCH] Let users pick the JSON library --- docs/client.md | 20 ++++ docs/migration/ogen.md | 3 +- docs/server.md | 32 +++++- examples/bodies/checked/gen.go | 23 +++-- examples/bodies/defaults/gen.go | 23 +++-- examples/bodies/encoding/gen.go | 57 ++++++++--- examples/bodies/ranges/gen.go | 61 ++++++++---- examples/bodies/responses/gen.go | 55 ++++++++--- examples/client/basic/gen.go | 41 +++++--- examples/client/basic/gen_test.go | 22 +++++ examples/client/bodies/gen.go | 55 +++++++---- examples/client/envelope/gen.go | 43 +++++--- examples/client/errors/gen.go | 39 ++++++-- examples/client/params/gen.go | 41 +++++--- examples/client/paths/gen.go | 39 ++++++-- examples/client/streaming/gen.go | 52 +++++++--- examples/layout/by-family/client/gen.go | 35 +++++-- examples/layout/by-family/server/gen.go | 23 +++-- .../layout/client-package/client/v2/gen.go | 39 ++++++-- examples/layout/service-apart/api/gen.go | 23 +++-- examples/layout/split-files/server.gen.go | 23 +++-- examples/layout/two-packages/api/gen.go | 57 ++++++++--- examples/mcp/basic/gen.go | 62 ++++++++---- examples/mcp/defaults/gen.go | 50 +++++++--- examples/mcp/discriminator/gen.go | 54 ++++++++--- examples/mcp/files/gen.go | 52 +++++++--- examples/mcp/nullable/gen.go | 54 ++++++++--- examples/mcp/patterns/gen.go | 54 ++++++++--- examples/mcp/readonly/gen.go | 54 ++++++++--- examples/mcp/results/gen.go | 56 ++++++++--- examples/mcp/select/gen.go | 60 ++++++++---- examples/mcp/streaming/gen.go | 60 ++++++++---- examples/migration/oapi-codegen-dd/gen.go | 66 +++++++++---- examples/migration/oapi-codegen/gen.go | 67 +++++++++---- examples/migration/ogen/gen.go | 61 ++++++++---- examples/models/no-descriptions/gen.go | 33 +++++-- examples/models/nullable/gen.go | 55 ++++++++--- examples/server/beego/basic/gen.go | 22 +++-- examples/server/beego/bodies/gen.go | 22 +++-- examples/server/beego/bodies/gen_test.go | 43 +++++--- examples/server/beego/errors/gen.go | 22 +++-- examples/server/beego/params/gen.go | 20 +++- examples/server/beego/paths/gen.go | 20 +++- examples/server/beego/scaffolds/gen.go | 22 +++-- examples/server/beego/split/api/gen.go | 21 ++-- examples/server/chi/basic/gen.go | 23 +++-- examples/server/chi/bodies/gen.go | 23 +++-- examples/server/chi/bodies/gen_test.go | 43 +++++--- examples/server/chi/errors/gen.go | 23 +++-- examples/server/chi/params/gen.go | 21 ++-- examples/server/chi/paths/gen.go | 21 ++-- examples/server/chi/scaffolds/gen.go | 23 +++-- examples/server/chi/split/api/gen.go | 21 ++-- examples/server/contract/gen.go | 23 +++-- examples/server/echo/basic/gen.go | 23 +++-- examples/server/echo/bodies/gen.go | 23 +++-- examples/server/echo/bodies/gen_test.go | 43 +++++--- examples/server/echo/errors/gen.go | 23 +++-- examples/server/echo/params/gen.go | 21 ++-- examples/server/echo/paths/gen.go | 21 ++-- examples/server/echo/scaffolds/gen.go | 23 +++-- examples/server/echo/split/api/gen.go | 21 ++-- examples/server/echov5/basic/gen.go | 23 +++-- examples/server/echov5/bodies/gen.go | 23 +++-- examples/server/echov5/bodies/gen_test.go | 43 +++++--- examples/server/echov5/errors/gen.go | 23 +++-- examples/server/echov5/params/gen.go | 21 ++-- examples/server/echov5/paths/gen.go | 21 ++-- examples/server/echov5/scaffolds/gen.go | 23 +++-- examples/server/echov5/split/api/gen.go | 21 ++-- examples/server/fasthttp/basic/gen.go | 23 +++-- examples/server/fasthttp/bodies/gen.go | 23 +++-- examples/server/fasthttp/bodies/gen_test.go | 43 +++++--- examples/server/fasthttp/errors/gen.go | 23 +++-- examples/server/fasthttp/params/gen.go | 21 ++-- examples/server/fasthttp/paths/gen.go | 21 ++-- examples/server/fasthttp/scaffolds/gen.go | 23 +++-- examples/server/fasthttp/split/api/gen.go | 21 ++-- examples/server/fiber/basic/gen.go | 23 +++-- examples/server/fiber/bodies/gen.go | 23 +++-- examples/server/fiber/bodies/gen_test.go | 43 +++++--- examples/server/fiber/errors/gen.go | 23 +++-- examples/server/fiber/params/gen.go | 21 ++-- examples/server/fiber/paths/gen.go | 21 ++-- examples/server/fiber/scaffolds/gen.go | 23 +++-- examples/server/fiber/split/api/gen.go | 21 ++-- examples/server/gin/basic/gen.go | 23 +++-- examples/server/gin/bodies/gen.go | 23 +++-- examples/server/gin/bodies/gen_test.go | 43 +++++--- examples/server/gin/errors/gen.go | 23 +++-- examples/server/gin/params/gen.go | 21 ++-- examples/server/gin/paths/gen.go | 21 ++-- examples/server/gin/scaffolds/gen.go | 23 +++-- examples/server/gin/split/api/gen.go | 21 ++-- examples/server/goframe/basic/gen.go | 23 +++-- examples/server/goframe/bodies/gen.go | 23 +++-- examples/server/goframe/bodies/gen_test.go | 43 +++++--- examples/server/goframe/errors/gen.go | 23 +++-- examples/server/goframe/params/gen.go | 21 ++-- examples/server/goframe/paths/gen.go | 21 ++-- examples/server/goframe/scaffolds/gen.go | 23 +++-- examples/server/goframe/split/api/gen.go | 21 ++-- examples/server/gorillamux/basic/gen.go | 23 +++-- examples/server/gorillamux/bodies/gen.go | 23 +++-- examples/server/gorillamux/bodies/gen_test.go | 43 +++++--- examples/server/gorillamux/errors/gen.go | 23 +++-- examples/server/gorillamux/params/gen.go | 21 ++-- examples/server/gorillamux/paths/gen.go | 21 ++-- examples/server/gorillamux/scaffolds/gen.go | 23 +++-- examples/server/gorillamux/split/api/gen.go | 21 ++-- examples/server/gozero/basic/gen.go | 23 +++-- examples/server/gozero/bodies/gen.go | 23 +++-- examples/server/gozero/bodies/gen_test.go | 43 +++++--- examples/server/gozero/errors/gen.go | 23 +++-- examples/server/gozero/params/gen.go | 21 ++-- examples/server/gozero/paths/gen.go | 21 ++-- examples/server/gozero/scaffolds/gen.go | 23 +++-- examples/server/gozero/split/api/gen.go | 21 ++-- examples/server/hertz/basic/gen.go | 23 +++-- examples/server/hertz/bodies/gen.go | 23 +++-- examples/server/hertz/bodies/gen_test.go | 43 +++++--- examples/server/hertz/errors/gen.go | 23 +++-- examples/server/hertz/params/gen.go | 21 ++-- examples/server/hertz/paths/gen.go | 21 ++-- examples/server/hertz/scaffolds/gen.go | 23 +++-- examples/server/hertz/split/api/gen.go | 21 ++-- examples/server/iris/basic/gen.go | 23 +++-- examples/server/iris/bodies/gen.go | 23 +++-- examples/server/iris/bodies/gen_test.go | 43 +++++--- examples/server/iris/errors/gen.go | 23 +++-- examples/server/iris/params/gen.go | 21 ++-- examples/server/iris/paths/gen.go | 21 ++-- examples/server/iris/scaffolds/gen.go | 23 +++-- examples/server/iris/split/api/gen.go | 21 ++-- examples/server/kratos/basic/gen.go | 23 +++-- examples/server/kratos/bodies/gen.go | 23 +++-- examples/server/kratos/bodies/gen_test.go | 43 +++++--- examples/server/kratos/errors/gen.go | 23 +++-- examples/server/kratos/params/gen.go | 21 ++-- examples/server/kratos/paths/gen.go | 21 ++-- examples/server/kratos/scaffolds/gen.go | 23 +++-- examples/server/kratos/split/api/gen.go | 21 ++-- examples/server/stdhttp/basic/gen.go | 23 +++-- examples/server/stdhttp/bodies/gen.go | 23 +++-- examples/server/stdhttp/bodies/gen_test.go | 43 +++++--- examples/server/stdhttp/errors/gen.go | 23 +++-- examples/server/stdhttp/params/gen.go | 21 ++-- examples/server/stdhttp/paths/gen.go | 21 ++-- examples/server/stdhttp/routes/gen.go | 23 +++-- examples/server/stdhttp/scaffolds/gen.go | 23 +++-- examples/server/stdhttp/split/api/gen.go | 21 ++-- examples/templates/blocks/gen.go | 23 +++-- examples/templates/wrapper/gen.go | 23 +++-- examples/unions/server/gen.go | 55 ++++++++--- internal/gen/client/client_test.go | 2 +- internal/gen/client/core.go | 2 + internal/gen/client/core_test.go | 1 + internal/gen/client/operations.go | 8 +- internal/gen/client/operations_test.go | 8 +- internal/gen/client/templates/core.tmpl | 30 +++++- internal/gen/client/templates/operations.tmpl | 17 ++-- .../gen/client/testdata/client.core.golden | 29 +++++- .../client/testdata/client.operations.golden | 42 ++++---- .../testdata/client.operations.plain.golden | 26 ++--- internal/gen/server/adapter.go | 4 +- internal/gen/server/templates/adapter.tmpl | 19 +++- internal/gen/server/templates/body.tmpl | 4 +- .../testdata/server.adapter.echo-v5.golden | 27 ++++-- .../testdata/server.adapter.echo.golden | 27 ++++-- .../gen/server/testdata/server.adapter.golden | 27 ++++-- .../testdata/server.adapter.kratos.golden | 27 ++++-- internal/gomodel/model.go | 4 +- internal/gomodel/model_test.go | 10 +- pkg/codegen/generate_test.go | 18 ++-- pkg/runtime/body_decode.go | 8 +- pkg/runtime/body_decode_test.go | 2 +- pkg/runtime/httpclient/httpclient.go | 6 +- pkg/runtime/httpclient/httpclient_test.go | 7 +- pkg/runtime/httpclient/response.go | 21 ++-- pkg/runtime/httpclient/response_test.go | 23 ++--- pkg/runtime/httpclient/stream.go | 52 ++++++---- pkg/runtime/httpclient/stream_test.go | 7 +- pkg/runtime/httpserver/write.go | 97 +++++++++++-------- pkg/runtime/httpserver/write_test.go | 55 ++++++++--- 184 files changed, 3614 insertions(+), 1553 deletions(-) diff --git a/docs/client.md b/docs/client.md index 5312f8a0..b74c2293 100644 --- a/docs/client.md +++ b/docs/client.md @@ -23,6 +23,7 @@ func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error) func WithHTTPClient(d HTTPDoer) PetClientOption func WithTimeout(d time.Duration) PetClientOption func WithRequestEditor(fns ...RequestEditor) PetClientOption +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) PetClientOption ``` ### Base URL @@ -61,6 +62,24 @@ that returns an error stops the request. Editors are the place for credentials. A method takes editors of its own too, see [Methods](#methods). +### JSON library + +The client writes and reads JSON with `encoding/json`. `WithJSON` swaps in another library that +has the same two functions: + +```go +import "github.com/bytedance/sonic" + +c, err := NewPetClient(baseURL, WithJSON(sonic.Marshal, sonic.Unmarshal)) +``` + +| Uses the library | Stays on `encoding/json` | +|---|---| +| JSON request bodies | parameters with JSON content | +| JSON response bodies and stream frames | the `MarshalJSON` methods of generated types, which the library calls | + +A nil function panics at once, as a nil `HTTPDoer` does. + ### Operation ID The context of a request holds the name of its operation, `ListPets`. Editors and the `HTTPDoer` @@ -552,6 +571,7 @@ return stream.Err() | `Close()` | may be called from another goroutine to end a pending `Next`, which then returns false | | `Sentinels` | frames that end the stream instead of being decoded, see below | | `MaxFrameSize` | the size limit of one frame, see below | +| `Unmarshal` | what decodes a frame, the client's [JSON library](#json-library) in `Stream` and `json.Unmarshal` when nil | #### Err diff --git a/docs/migration/ogen.md b/docs/migration/ogen.md index f4e898b4..267340f7 100644 --- a/docs/migration/ogen.md +++ b/docs/migration/ogen.md @@ -18,7 +18,8 @@ are built and tested. - The params of an operation sit in one options struct, with a field for each place they go: `PathParams`, `Query`, `Headers` and `Cookies`. The body is `Body` in the same struct, not a second argument. -- JSON goes through `encoding/json`, not `jx`. +- JSON goes through `encoding/json`, not `jx`. `WithJSON` on the client, and `WithJSONDecoder` and + `WithJSONEncoder` on the server, take another library such as sonic. - A `uuid`, `uri` or `ipv4` format is a `string` checked by `Validate`, unless `models.format-types` names a type for it. - There is no security handler and no OpenTelemetry. A middleware checks credentials, and a diff --git a/docs/server.md b/docs/server.md index 4a840fe5..a949afda 100644 --- a/docs/server.md +++ b/docs/server.md @@ -363,11 +363,33 @@ Options are set with `ServerOption` functions on the adapter and on the router a |---|---| | `WithMiddleware(mw...)` | `func(http.Handler) http.Handler` wrappers, outermost first | | `WithErrorHandler(h)` | what writes failed requests, `DefaultErrorHandler{}` by default | -| `WithJSONDecoder(fn)` | what reads JSON bodies, `runtime.DecodeJSON` by default | +| `WithJSONDecoder(fn)` | what reads JSON request bodies, `json.Unmarshal` by default | +| `WithJSONEncoder(fn)` | what writes JSON response bodies and stream frames, `json.Marshal` by default | | `WithPresence(p)` | what checks the keys of request bodies and fills their defaults, see [request bodies](#request-bodies) | | `WithMultipartMaxMemory(n)` | memory for multipart forms, `server.multipart-max-memory` by default | | `WithRouter(r)` | the router the routes go on, one of the framework's | +### JSON library + +`WithJSONDecoder` and `WithJSONEncoder` take the two functions of any library shaped like +`encoding/json`: + +```go +import "github.com/bytedance/sonic" + +router := NewRouter(svc, + WithJSONDecoder(sonic.Unmarshal), + WithJSONEncoder(sonic.Marshal), +) +``` + +The adapter still reads the body and checks it first. An empty required body is a 400 before the +decoder runs. + +These stay on `encoding/json`: parameters with JSON content, the bodies `DefaultErrorHandler` +writes, the key checks of [request bodies](#request-bodies), and the `MarshalJSON` methods of +generated types, which the library calls. + ## Validation ```yaml @@ -1101,7 +1123,8 @@ goes as it is. ### Body codecs -- `DecodeJSON`, `DecodeText`, `DecodeBytes` and `DecodeFile`. +- `DecodeJSON` reads a body with the unmarshal function it is given. `DecodeText`, `DecodeBytes` + and `DecodeFile`. - `DecodeForm`: bracketed keys nest, `address[city]=Berlin`, `items[0]=a`. One value for a struct or map is read as JSON, else as a string. A property its `Encoding` gives a style is read as a query parameter of that style. @@ -1119,8 +1142,9 @@ goes as it is. Five packages sit beside it: - `pkg/runtime/httpserver` holds what a generated server needs beside the codecs: `HandlerError` - and the error handlers, and `Write`, which sends a status, headers and a body. It writes JSON for - most values, text and bytes as they are, and streams a `File`. + and the error handlers, and `Writer`, whose `Write` sends a status, headers and a body. It writes + JSON for most values with its `Marshal`, `json.Marshal` when nil, text and bytes as they are, + and streams a `File`. - `pkg/runtime/httpclient` holds the requests and responses of a client, see [client](client.md#runtime). - `pkg/runtime/validation` holds the checks of `Validate` and the errors they return (see diff --git a/examples/bodies/checked/gen.go b/examples/bodies/checked/gen.go index c0d7e3f4..10452688 100644 --- a/examples/bodies/checked/gen.go +++ b/examples/bodies/checked/gen.go @@ -4,8 +4,8 @@ package checked import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -260,7 +260,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -272,7 +273,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -296,13 +298,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -346,7 +355,7 @@ func (a *HTTPAdapter) AddPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "AddPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.BodyJSON, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "AddPet", err) return } @@ -445,7 +454,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/bodies/defaults/gen.go b/examples/bodies/defaults/gen.go index 65f0dedc..e9210be7 100644 --- a/examples/bodies/defaults/gen.go +++ b/examples/bodies/defaults/gen.go @@ -4,8 +4,8 @@ package defaults import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -151,7 +151,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -163,7 +164,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -187,13 +189,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -237,7 +246,7 @@ func (a *HTTPAdapter) AddOrder(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "AddOrder", err) return } - if err = a.opts.JSONDecoder(body, &opts.BodyJSON, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "AddOrder", err) return } @@ -293,7 +302,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/bodies/encoding/gen.go b/examples/bodies/encoding/gen.go index 8e069a2c..25d990e1 100644 --- a/examples/bodies/encoding/gen.go +++ b/examples/bodies/encoding/gen.go @@ -4,8 +4,8 @@ package encoding import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "net/url" @@ -565,7 +565,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -577,7 +578,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -601,13 +603,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -835,7 +844,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -975,13 +984,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -991,7 +1012,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -1012,7 +1039,7 @@ func (c *Client) SendParcel(ctx context.Context, opts *SendParcelRequestOptions, var out *Receipt if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -1056,7 +1083,7 @@ func (c *Client) GetParcel(ctx context.Context, opts *GetParcelRequestOptions, e var out *Receipt if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -1088,7 +1115,7 @@ func (c *Client) PrintLabel(ctx context.Context, opts *PrintLabelRequestOptions, var out *Label if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -1123,7 +1150,7 @@ func (c *Client) CreateCustomer(ctx context.Context, opts *CreateCustomerRequest var out *Customer if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/bodies/ranges/gen.go b/examples/bodies/ranges/gen.go index b587c175..7a2b0d98 100644 --- a/examples/bodies/ranges/gen.go +++ b/examples/bodies/ranges/gen.go @@ -4,8 +4,8 @@ package ranges import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -312,7 +312,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -323,7 +324,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -346,13 +348,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -487,7 +496,7 @@ func (a *HTTPAdapter) AddEvent(w http.ResponseWriter, r *http.Request) { a.failDecode(w, r, "AddEvent", runtime.ErrBodyEmpty) return case runtime.IsJSON(contentType): - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "AddEvent", err) return } @@ -522,7 +531,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -661,13 +670,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -677,7 +698,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -698,7 +725,7 @@ func (c *Client) AddAttachment(ctx context.Context, opts *AddAttachmentRequestOp var out *AddAttachmentResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -735,7 +762,7 @@ func (c *Client) PutScore(ctx context.Context, opts *PutScoreRequestOptions, edi var out *PutScoreResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -770,7 +797,7 @@ func (c *Client) AddCard(ctx context.Context, opts *AddCardRequestOptions, edito var out *AddCardResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -805,7 +832,7 @@ func (c *Client) AddEvent(ctx context.Context, opts *AddEventRequestOptions, edi var out *AddEventResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -819,7 +846,7 @@ func (c *Client) AddEventRequest(ctx context.Context, opts *AddEventRequestOptio b := httpclient.NewRequestBuilder(http.MethodPost, "/events") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/bodies/responses/gen.go b/examples/bodies/responses/gen.go index 0bdaacc2..695cea9f 100644 --- a/examples/bodies/responses/gen.go +++ b/examples/bodies/responses/gen.go @@ -4,8 +4,8 @@ package responses import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -230,7 +230,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -241,7 +242,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -264,13 +266,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -361,7 +370,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -478,13 +487,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -494,7 +515,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -515,7 +542,7 @@ func (c *Client) Ping(ctx context.Context, opts *PingRequestOptions, editors ... var out PingResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -544,7 +571,7 @@ func (c *Client) GetMotd(ctx context.Context, opts *GetMotdRequestOptions, edito var out *GetMotdResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/*", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -573,7 +600,7 @@ func (c *Client) GetProfile(ctx context.Context, opts *GetProfileRequestOptions, var out *Profile if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "multipart/*", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/client/basic/gen.go b/examples/client/basic/gen.go index 5ebc8b3b..649f964c 100644 --- a/examples/client/basic/gen.go +++ b/examples/client/basic/gen.go @@ -4,6 +4,7 @@ package basic import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -141,13 +142,25 @@ func WithRequestEditor(fns ...RequestEditor) PetClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) PetClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *PetClient) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // PetClient calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type PetClient struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewPetClient returns a client of the API at baseURL. @@ -157,7 +170,13 @@ func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error) { return nil, err } - c := &PetClient{baseURL: u, doer: &http.Client{}, timeout: 5 * time.Second} + c := &PetClient{ + baseURL: u, + doer: &http.Client{}, + timeout: 5 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -178,7 +197,7 @@ func (c *PetClient) ListPets(ctx context.Context, opts *ListPetsRequestOptions, var out ListPetsResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -210,7 +229,7 @@ func (c *PetClient) CreatePet(ctx context.Context, opts *CreatePetRequestOptions var out *Pet if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -224,7 +243,7 @@ func (c *PetClient) CreatePetRequest(ctx context.Context, opts *CreatePetRequest b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -245,7 +264,7 @@ func (c *PetClient) GetPet(ctx context.Context, opts *GetPetRequestOptions, edit var out *Pet if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -273,7 +292,7 @@ func (c *PetClient) DeletePet(ctx context.Context, opts *DeletePetRequestOptions if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeletePetRequest builds the request of DELETE /pets/{id}. @@ -302,7 +321,7 @@ func (c *PetClient) Ping(ctx context.Context, opts *PingRequestOptions, editors var out *PingResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/client/basic/gen_test.go b/examples/client/basic/gen_test.go index 2b57948d..120893d3 100644 --- a/examples/client/basic/gen_test.go +++ b/examples/client/basic/gen_test.go @@ -260,6 +260,28 @@ func TestOptions(t *testing.T) { assert.Equal(t, "abc pets/1", <-seen, "the editors run in order") } +func TestJSONOption(t *testing.T) { + t.Parallel() + + var calls []string + marshal := func(v any) ([]byte, error) { + calls = append(calls, "marshal") + return json.Marshal(v) + } + unmarshal := func(data []byte, v any) error { + calls = append(calls, "unmarshal") + return json.Unmarshal(data, v) + } + c := newClient(t, WithJSON(marshal, unmarshal)) + + created, err := c.CreatePet(context.Background(), &CreatePetRequestOptions{Body: &Pet{ID: 1, Name: "Rex"}}) + + require.NoError(t, err) + assert.Equal(t, &Pet{ID: 1, Name: "Rex"}, created) + assert.Equal(t, []string{"marshal", "unmarshal"}, calls) + assert.PanicsWithValue(t, "WithJSON: nil function", func() { WithJSON(nil, unmarshal) }) +} + func TestCallEditors(t *testing.T) { t.Parallel() diff --git a/examples/client/bodies/gen.go b/examples/client/bodies/gen.go index 29d7a11d..f47bfcd8 100644 --- a/examples/client/bodies/gen.go +++ b/examples/client/bodies/gen.go @@ -4,6 +4,7 @@ package bodies import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -233,13 +234,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -249,7 +262,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -270,7 +289,7 @@ func (c *Client) PostJSON(ctx context.Context, opts *PostJSONRequestOptions, edi var out *Note if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -284,7 +303,7 @@ func (c *Client) PostJSONRequest(ctx context.Context, opts *PostJSONRequestOptio b := httpclient.NewRequestBuilder(http.MethodPost, "/json") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) } return c.newRequest(ctx, "PostJSON", b, editors) } @@ -303,7 +322,7 @@ func (c *Client) GetForm(ctx context.Context, opts *GetFormRequestOptions, edito var out *Note if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-www-form-urlencoded", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -332,7 +351,7 @@ func (c *Client) PostForm(ctx context.Context, opts *PostFormRequestOptions, edi var out *Note if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -367,7 +386,7 @@ func (c *Client) PostCharset(ctx context.Context, opts *PostCharsetRequestOption var out *Note if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json; charset=utf-8", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -381,7 +400,7 @@ func (c *Client) PostCharsetRequest(ctx context.Context, opts *PostCharsetReques b := httpclient.NewRequestBuilder(http.MethodPost, "/charset") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json; charset=utf-8") + b.JSONBody(opts.Body, "application/json; charset=utf-8", c.marshal) } return c.newRequest(ctx, "PostCharset", b, editors) } @@ -400,7 +419,7 @@ func (c *Client) Upload(ctx context.Context, opts *UploadRequestOptions, editors var out UploadResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -435,7 +454,7 @@ func (c *Client) PostText(ctx context.Context, opts *PostTextRequestOptions, edi var out *PostTextResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -470,7 +489,7 @@ func (c *Client) PutFile(ctx context.Context, opts *PutFileRequestOptions, edito var out *PutFileResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "image/png", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -501,7 +520,7 @@ func (c *Client) PutXML(ctx context.Context, opts *PutXMLRequestOptions, editors if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // PutXMLRequest builds the request of PUT /xml. @@ -531,7 +550,7 @@ func (c *Client) PostAny(ctx context.Context, opts *PostAnyRequestOptions, edito var out *PostAnyResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -568,7 +587,7 @@ func (c *Client) GetAnyText(ctx context.Context, opts *GetAnyTextRequestOptions, var out *GetAnyTextResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "*/*", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -597,7 +616,7 @@ func (c *Client) GetAnyBytes(ctx context.Context, opts *GetAnyBytesRequestOption var out []byte if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "*/*", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/client/envelope/gen.go b/examples/client/envelope/gen.go index 3da31b3e..f8749349 100644 --- a/examples/client/envelope/gen.go +++ b/examples/client/envelope/gen.go @@ -4,6 +4,7 @@ package envelope import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -217,13 +218,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -233,7 +246,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -258,7 +277,7 @@ func (c *Client) SubmitJob(ctx context.Context, opts *SubmitJobRequestOptions, e {Status: "201", MediaType: "application/json", Dst: &out}, {Status: "202"}, {Status: "400", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -282,7 +301,7 @@ func (c *Client) SubmitJobWithResponse(ctx context.Context, opts *SubmitJobReque {Status: "400", MediaType: "application/problem+json", Dst: &out.ProblemJSON400}, {Status: "201", IsHeaders: true, Dst: &out.Headers201}, {Status: "202", IsHeaders: true, Dst: &out.Headers202}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -296,7 +315,7 @@ func (c *Client) SubmitJobRequest(ctx context.Context, opts *SubmitJobRequestOpt b := httpclient.NewRequestBuilder(http.MethodPost, "/jobs") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -317,7 +336,7 @@ func (c *Client) GetJobLog(ctx context.Context, opts *GetJobLogRequestOptions, e var out GetJobLogJSONResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -338,7 +357,7 @@ func (c *Client) GetJobLogWithResponse(ctx context.Context, opts *GetJobLogReque if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out.Text200}, {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -372,7 +391,7 @@ func (c *Client) CancelJob(ctx context.Context, opts *CancelJobRequestOptions, e return httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "4XX", MediaType: "application/problem+json", Dst: new(Problem)}, {Status: "410"}, - }) + }, c.unmarshal) } // CancelJobWithResponse calls DELETE /jobs/{id} and returns the whole response. @@ -390,7 +409,7 @@ func (c *Client) CancelJobWithResponse(ctx context.Context, opts *CancelJobReque if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "4XX", MediaType: "application/problem+json", Dst: &out.ProblemJSON4XX}, {Status: "410"}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil diff --git a/examples/client/errors/gen.go b/examples/client/errors/gen.go index 38bd696e..459e4523 100644 --- a/examples/client/errors/gen.go +++ b/examples/client/errors/gen.go @@ -4,6 +4,7 @@ package errors import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -207,13 +208,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -223,7 +236,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -246,7 +265,7 @@ func (c *Client) AddPet(ctx context.Context, opts *AddPetRequestOptions, editors {Status: "201", MediaType: "application/json", Dst: &out}, {Status: "204"}, {Status: "default", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -260,7 +279,7 @@ func (c *Client) AddPetRequest(ctx context.Context, opts *AddPetRequestOptions, b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -282,7 +301,7 @@ func (c *Client) GetPet(ctx context.Context, opts *GetPetRequestOptions, editors if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "404", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -317,7 +336,7 @@ func (c *Client) PutPet(ctx context.Context, opts *PutPetRequestOptions, editors var out *Pet if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -334,7 +353,7 @@ func (c *Client) PutPetRequest(ctx context.Context, opts *PutPetRequestOptions, } switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/client/params/gen.go b/examples/client/params/gen.go index 22ffc25e..71988d84 100644 --- a/examples/client/params/gen.go +++ b/examples/client/params/gen.go @@ -4,6 +4,7 @@ package params import ( "context" + "encoding/json" "mime/multipart" "net/http" "net/url" @@ -402,13 +403,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -418,7 +431,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -439,7 +458,7 @@ func (c *Client) PathStyles(ctx context.Context, opts *PathStylesRequestOptions, var out Echo if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -474,7 +493,7 @@ func (c *Client) QueryStyles(ctx context.Context, opts *QueryStylesRequestOption var out Echo if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -518,7 +537,7 @@ func (c *Client) HeaderStyles(ctx context.Context, opts *HeaderStylesRequestOpti var out Echo if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -553,7 +572,7 @@ func (c *Client) CookieStyles(ctx context.Context, opts *CookieStylesRequestOpti var out Echo if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -587,7 +606,7 @@ func (c *Client) Search(ctx context.Context, opts *SearchRequestOptions, editors var out Echo if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -617,7 +636,7 @@ func (c *Client) Find(ctx context.Context, opts *FindRequestOptions, editors ... var out Echo if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/client/paths/gen.go b/examples/client/paths/gen.go index 753e9152..ebb18785 100644 --- a/examples/client/paths/gen.go +++ b/examples/client/paths/gen.go @@ -4,6 +4,7 @@ package paths import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -171,13 +172,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -187,7 +200,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -204,7 +223,7 @@ func (c *Client) SearchPhotos(ctx context.Context, opts *SearchPhotosRequestOpti if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // SearchPhotosRequest builds the request of GET /rest?method=photos.search. @@ -229,7 +248,7 @@ func (c *Client) ListOrders(ctx context.Context, opts *ListOrdersRequestOptions, if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // ListOrdersRequest builds the request of GET /orders?end={end}&page={page}. @@ -255,7 +274,7 @@ func (c *Client) ShareFile(ctx context.Context, opts *ShareFileRequestOptions, e if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // ShareFileRequest builds the request of PUT /files/{id}#share. @@ -280,7 +299,7 @@ func (c *Client) ListUsers(ctx context.Context, opts *ListUsersRequestOptions, e if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // ListUsersRequest builds the request of GET /#Action=ListUsers. @@ -305,7 +324,7 @@ func (c *Client) Search(ctx context.Context, opts *SearchRequestOptions, editors if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // SearchRequest builds the request of GET /search?query={query}. diff --git a/examples/client/streaming/gen.go b/examples/client/streaming/gen.go index 74f9969d..8bc96c0e 100644 --- a/examples/client/streaming/gen.go +++ b/examples/client/streaming/gen.go @@ -4,6 +4,7 @@ package streaming import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -241,13 +242,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -257,7 +270,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -281,7 +300,7 @@ func (c *Client) Chat(ctx context.Context, opts *ChatRequestOptions, editors ... if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "400", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -304,7 +323,7 @@ func (c *Client) ChatWithResponse(ctx context.Context, opts *ChatRequestOptions, {Status: "200", MediaType: "text/event-stream", Dst: &out.EventStream200}, {Status: "400", MediaType: "application/problem+json", Dst: &out.ProblemJSON400}, {Status: "200", IsHeaders: true, Dst: &out.Headers200}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -322,7 +341,7 @@ func (c *Client) ChatStream(ctx context.Context, opts *ChatRequestOptions, edito } return httpclient.OpenStream[Chunk](res, body, []httpclient.ResponseTarget{ {Status: "400", MediaType: "application/problem+json", Dst: new(Problem)}, - }) + }, c.unmarshal) } // ChatStreamWithResponse calls POST /chat and returns the whole response, with its stream. @@ -342,7 +361,7 @@ func (c *Client) ChatStreamWithResponse(ctx context.Context, opts *ChatRequestOp {Status: "200", MediaType: "text/event-stream", Dst: &out.EventStream200}, {Status: "400", MediaType: "application/problem+json", Dst: &out.ProblemJSON400}, {Status: "200", IsHeaders: true, Dst: &out.Headers200}, - }) + }, c.unmarshal) if err != nil { return out, err } @@ -357,7 +376,7 @@ func (c *Client) ChatRequest(ctx context.Context, opts *ChatRequestOptions, edit b := httpclient.NewRequestBuilder(http.MethodPost, "/chat") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -375,7 +394,7 @@ func (c *Client) ListEvents(ctx context.Context, opts *ListEventsRequestOptions, if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // ListEventsWithResponse calls GET /events and returns the whole response. @@ -403,7 +422,7 @@ func (c *Client) ListEventsStream(ctx context.Context, opts *ListEventsRequestOp if err != nil { return nil, err } - return httpclient.OpenStream[ListEventsResponseItem](res, body, nil) + return httpclient.OpenStream[ListEventsResponseItem](res, body, nil, c.unmarshal) } // ListEventsStreamWithResponse calls GET /events and returns the whole response, with its stream. @@ -420,6 +439,7 @@ func (c *Client) ListEventsStreamWithResponse(ctx context.Context, opts *ListEve out := &ListEventsResponse{HTTPResponse: res, Body: body} if httpclient.IsStreaming(res) { out.Stream200 = httpclient.NewStream[ListEventsResponseItem](res) + out.Stream200.Unmarshal = c.unmarshal } return out, nil } @@ -449,7 +469,7 @@ func (c *Client) TailLog(ctx context.Context, opts *TailLogRequestOptions, edito if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out}, {Status: "404", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -470,7 +490,7 @@ func (c *Client) TailLogWithResponse(ctx context.Context, opts *TailLogRequestOp if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out.Ndjson200}, {Status: "404", MediaType: "application/problem+json", Dst: &out.ProblemJSON404}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -488,7 +508,7 @@ func (c *Client) TailLogStream(ctx context.Context, opts *TailLogRequestOptions, } return httpclient.OpenStream[[]byte](res, body, []httpclient.ResponseTarget{ {Status: "404", MediaType: "application/problem+json", Dst: new(Problem)}, - }) + }, c.unmarshal) } // TailLogStreamWithResponse calls GET /logs/{job} and returns the whole response, with its stream. @@ -506,7 +526,7 @@ func (c *Client) TailLogStreamWithResponse(ctx context.Context, opts *TailLogReq out.Stream200, err = httpclient.DecodeStream[[]byte](res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out.Ndjson200}, {Status: "404", MediaType: "application/problem+json", Dst: &out.ProblemJSON404}, - }) + }, c.unmarshal) if err != nil { return out, err } diff --git a/examples/layout/by-family/client/gen.go b/examples/layout/by-family/client/gen.go index d282c1bb..d252c048 100644 --- a/examples/layout/by-family/client/gen.go +++ b/examples/layout/by-family/client/gen.go @@ -4,6 +4,7 @@ package client import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -96,13 +97,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -112,7 +125,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -133,7 +152,7 @@ func (c *Client) CreateOrder(ctx context.Context, opts *CreateOrderRequestOption var out *models.Order if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -147,7 +166,7 @@ func (c *Client) CreateOrderRequest(ctx context.Context, opts *CreateOrderReques b := httpclient.NewRequestBuilder(http.MethodPost, "/orders") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -168,7 +187,7 @@ func (c *Client) GetOrder(ctx context.Context, opts *GetOrderRequestOptions, edi var out *models.Order if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/layout/by-family/server/gen.go b/examples/layout/by-family/server/gen.go index 6af7bc7c..936d9c80 100644 --- a/examples/layout/by-family/server/gen.go +++ b/examples/layout/by-family/server/gen.go @@ -4,8 +4,8 @@ package server import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/layout/by-family/models" @@ -177,7 +177,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -188,7 +189,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -211,13 +213,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -249,7 +258,7 @@ func (a *HTTPAdapter) CreateOrder(w http.ResponseWriter, r *http.Request) { opts := &CreateOrderServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateOrder", err) return } @@ -315,7 +324,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/layout/client-package/client/v2/gen.go b/examples/layout/client-package/client/v2/gen.go index 23426452..f32045aa 100644 --- a/examples/layout/client-package/client/v2/gen.go +++ b/examples/layout/client-package/client/v2/gen.go @@ -4,6 +4,7 @@ package orders import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -126,13 +127,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -142,7 +155,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -163,7 +182,7 @@ func (c *Client) CreateOrder(ctx context.Context, opts *CreateOrderRequestOption var out *models.Order if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -183,7 +202,7 @@ func (c *Client) CreateOrderWithResponse(ctx context.Context, opts *CreateOrderR out := &CreateOrderResponse{HTTPResponse: res, Body: body} if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out.JSON201}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -197,7 +216,7 @@ func (c *Client) CreateOrderRequest(ctx context.Context, opts *CreateOrderReques b := httpclient.NewRequestBuilder(http.MethodPost, "/orders") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -218,7 +237,7 @@ func (c *Client) GetOrder(ctx context.Context, opts *GetOrderRequestOptions, edi var out *models.Order if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -239,7 +258,7 @@ func (c *Client) GetOrderWithResponse(ctx context.Context, opts *GetOrderRequest if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "404", MediaType: "application/json", Dst: &out.JSON404}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil diff --git a/examples/layout/service-apart/api/gen.go b/examples/layout/service-apart/api/gen.go index 5118ab69..4e1c18f7 100644 --- a/examples/layout/service-apart/api/gen.go +++ b/examples/layout/service-apart/api/gen.go @@ -3,8 +3,8 @@ package api import ( + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/layout/service-apart/models" @@ -39,7 +39,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -50,7 +51,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -73,13 +75,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -111,7 +120,7 @@ func (a *HTTPAdapter) CreateOrder(w http.ResponseWriter, r *http.Request) { opts := &service.CreateOrderServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateOrder", err) return } @@ -177,7 +186,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/layout/split-files/server.gen.go b/examples/layout/split-files/server.gen.go index e183147a..fa9b2654 100644 --- a/examples/layout/split-files/server.gen.go +++ b/examples/layout/split-files/server.gen.go @@ -4,8 +4,8 @@ package orders import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -176,7 +176,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -187,7 +188,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -210,13 +212,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -248,7 +257,7 @@ func (a *HTTPAdapter) CreateOrder(w http.ResponseWriter, r *http.Request) { opts := &CreateOrderServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateOrder", err) return } @@ -314,7 +323,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/layout/two-packages/api/gen.go b/examples/layout/two-packages/api/gen.go index 00d4e55f..a019809a 100644 --- a/examples/layout/two-packages/api/gen.go +++ b/examples/layout/two-packages/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -181,7 +181,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -192,7 +193,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -215,13 +217,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -253,7 +262,7 @@ func (a *HTTPAdapter) CreateOrder(w http.ResponseWriter, r *http.Request) { opts := &CreateOrderServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateOrder", err) return } @@ -319,7 +328,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -437,13 +446,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -453,7 +474,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -474,7 +501,7 @@ func (c *Client) CreateOrder(ctx context.Context, opts *CreateOrderRequestOption var out *models.Order if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -488,7 +515,7 @@ func (c *Client) CreateOrderRequest(ctx context.Context, opts *CreateOrderReques b := httpclient.NewRequestBuilder(http.MethodPost, "/orders") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -509,7 +536,7 @@ func (c *Client) GetOrder(ctx context.Context, opts *GetOrderRequestOptions, edi var out *models.Order if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/mcp/basic/gen.go b/examples/mcp/basic/gen.go index 45ac1ce1..88070961 100644 --- a/examples/mcp/basic/gen.go +++ b/examples/mcp/basic/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -462,7 +461,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -473,7 +473,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -496,13 +497,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -557,7 +565,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -666,7 +674,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -826,13 +834,25 @@ func WithRequestEditor(fns ...RequestEditor) PetClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) PetClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *PetClient) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // PetClient calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type PetClient struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewPetClient returns a client of the API at baseURL. @@ -842,7 +862,13 @@ func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error) { return nil, err } - c := &PetClient{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &PetClient{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -865,7 +891,7 @@ func (c *PetClient) ListPets(ctx context.Context, opts *ListPetsRequestOptions, var out ListPetsResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -899,7 +925,7 @@ func (c *PetClient) CreatePet(ctx context.Context, opts *CreatePetRequestOptions if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, {Status: "409", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -913,7 +939,7 @@ func (c *PetClient) CreatePetRequest(ctx context.Context, opts *CreatePetRequest b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -936,7 +962,7 @@ func (c *PetClient) GetPet(ctx context.Context, opts *GetPetRequestOptions, edit if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "404", MediaType: "application/problem+json", Dst: new(Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -965,7 +991,7 @@ func (c *PetClient) DeletePet(ctx context.Context, opts *DeletePetRequestOptions if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeletePetRequest builds the request of DELETE /pets/{id}. @@ -995,7 +1021,7 @@ func (c *PetClient) Ping(ctx context.Context, opts *PingRequestOptions, editors var out *PingResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/mcp/defaults/gen.go b/examples/mcp/defaults/gen.go index e160ce18..76577a81 100644 --- a/examples/mcp/defaults/gen.go +++ b/examples/mcp/defaults/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -167,7 +166,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -178,7 +178,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -201,13 +202,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -278,7 +286,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -377,13 +385,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -393,7 +413,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -415,7 +441,7 @@ func (c *Client) Search(ctx context.Context, opts *SearchRequestOptions, editors var out *Asked if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/mcp/discriminator/gen.go b/examples/mcp/discriminator/gen.go index c59d0a38..0576d5e5 100644 --- a/examples/mcp/discriminator/gen.go +++ b/examples/mcp/discriminator/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -178,7 +177,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -189,7 +189,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -212,13 +213,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -250,7 +258,7 @@ func (a *HTTPAdapter) AddAnimal(w http.ResponseWriter, r *http.Request) { opts := &AddAnimalServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "AddAnimal", err) return } @@ -288,7 +296,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -388,13 +396,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -404,7 +424,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -426,7 +452,7 @@ func (c *Client) AddAnimal(ctx context.Context, opts *AddAnimalRequestOptions, e var out *Animal if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -440,7 +466,7 @@ func (c *Client) AddAnimalRequest(ctx context.Context, opts *AddAnimalRequestOpt b := httpclient.NewRequestBuilder(http.MethodPost, "/animals") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/mcp/files/gen.go b/examples/mcp/files/gen.go index 6697ae0a..075b44e0 100644 --- a/examples/mcp/files/gen.go +++ b/examples/mcp/files/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -191,7 +190,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -202,7 +202,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -225,13 +226,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -339,7 +347,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -451,13 +459,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -467,7 +487,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -489,7 +515,7 @@ func (c *Client) SetPhoto(ctx context.Context, opts *SetPhotoRequestOptions, edi var out *Received if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -528,7 +554,7 @@ func (c *Client) AddNote(ctx context.Context, opts *AddNoteRequestOptions, edito var out *Received if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/mcp/nullable/gen.go b/examples/mcp/nullable/gen.go index 0cb2134a..38fff34e 100644 --- a/examples/mcp/nullable/gen.go +++ b/examples/mcp/nullable/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -156,7 +155,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -167,7 +167,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -190,13 +191,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -228,7 +236,7 @@ func (a *HTTPAdapter) Adopt(w http.ResponseWriter, r *http.Request) { opts := &AdoptServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Adopt", err) return } @@ -266,7 +274,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -366,13 +374,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -382,7 +402,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -404,7 +430,7 @@ func (c *Client) Adopt(ctx context.Context, opts *AdoptRequestOptions, editors . var out *Adoption if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -418,7 +444,7 @@ func (c *Client) AdoptRequest(ctx context.Context, opts *AdoptRequestOptions, ed b := httpclient.NewRequestBuilder(http.MethodPost, "/adoptions") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/mcp/patterns/gen.go b/examples/mcp/patterns/gen.go index 49d46438..10d5b5e2 100644 --- a/examples/mcp/patterns/gen.go +++ b/examples/mcp/patterns/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "regexp" @@ -169,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -180,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -203,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -247,7 +255,7 @@ func (a *HTTPAdapter) AddTag(w http.ResponseWriter, r *http.Request) { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "AddTag", err) return } @@ -285,7 +293,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -389,13 +397,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -405,7 +425,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -427,7 +453,7 @@ func (c *Client) AddTag(ctx context.Context, opts *AddTagRequestOptions, editors var out *Added if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -444,7 +470,7 @@ func (c *Client) AddTagRequest(ctx context.Context, opts *AddTagRequestOptions, } switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/mcp/readonly/gen.go b/examples/mcp/readonly/gen.go index dcc442fe..5fe4a5fc 100644 --- a/examples/mcp/readonly/gen.go +++ b/examples/mcp/readonly/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -123,7 +122,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -134,7 +134,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -157,13 +158,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -195,7 +203,7 @@ func (a *HTTPAdapter) AddPet(w http.ResponseWriter, r *http.Request) { opts := &AddPetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "AddPet", err) return } @@ -233,7 +241,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -329,13 +337,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -345,7 +365,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -367,7 +393,7 @@ func (c *Client) AddPet(ctx context.Context, opts *AddPetRequestOptions, editors var out *Pet if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -381,7 +407,7 @@ func (c *Client) AddPetRequest(ctx context.Context, opts *AddPetRequestOptions, b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/mcp/results/gen.go b/examples/mcp/results/gen.go index 6ead354a..7c21ecb4 100644 --- a/examples/mcp/results/gen.go +++ b/examples/mcp/results/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -310,7 +309,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -321,7 +321,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -344,13 +345,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -470,7 +478,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -605,13 +613,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -621,7 +641,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -643,7 +669,7 @@ func (c *Client) CountPets(ctx context.Context, opts *CountPetsRequestOptions, e var out *CountPetsResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -674,7 +700,7 @@ func (c *Client) FindPet(ctx context.Context, opts *FindPetRequestOptions, edito if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "204"}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -707,7 +733,7 @@ func (c *Client) GetPhoto(ctx context.Context, opts *GetPhotoRequestOptions, edi var out *GetPhotoResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "*/*", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -740,7 +766,7 @@ func (c *Client) GetIcon(ctx context.Context, opts *GetIconRequestOptions, edito var out []byte if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "image/png", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/examples/mcp/select/gen.go b/examples/mcp/select/gen.go index 84f35dbb..dc057340 100644 --- a/examples/mcp/select/gen.go +++ b/examples/mcp/select/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -321,7 +320,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -332,7 +332,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -355,13 +356,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -431,7 +439,7 @@ func (a *HTTPAdapter) PutItem(w http.ResponseWriter, r *http.Request) { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutItem", err) return } @@ -508,7 +516,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -648,13 +656,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -664,7 +684,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -686,7 +712,7 @@ func (c *Client) GetItem(ctx context.Context, opts *GetItemRequestOptions, edito var out *Item if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -725,7 +751,7 @@ func (c *Client) PutItem(ctx context.Context, opts *PutItemRequestOptions, edito var out *Item if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -742,7 +768,7 @@ func (c *Client) PutItemRequest(ctx context.Context, opts *PutItemRequestOptions } switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -760,7 +786,7 @@ func (c *Client) DeleteItem(ctx context.Context, opts *DeleteItemRequestOptions, if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeleteItemRequest builds the request of DELETE /items/{id}. @@ -786,7 +812,7 @@ func (c *Client) Reset(ctx context.Context, opts *ResetRequestOptions, editors . if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // ResetRequest builds the request of POST /internal/reset. diff --git a/examples/mcp/streaming/gen.go b/examples/mcp/streaming/gen.go index a967fb00..43a7e7e0 100644 --- a/examples/mcp/streaming/gen.go +++ b/examples/mcp/streaming/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "iter" "net/http" "net/url" @@ -194,7 +193,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -205,7 +205,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -228,13 +229,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -266,7 +274,7 @@ func (a *HTTPAdapter) Chat(w http.ResponseWriter, r *http.Request) { opts := &ChatServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Chat", err) return } @@ -321,7 +329,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -434,13 +442,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -450,7 +470,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -473,7 +499,7 @@ func (c *Client) Chat(ctx context.Context, opts *ChatRequestOptions, editors ... var out *Reply if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -489,7 +515,7 @@ func (c *Client) ChatStream(ctx context.Context, opts *ChatRequestOptions, edito if err != nil { return nil, err } - return httpclient.OpenStream[Chunk](res, body, nil) + return httpclient.OpenStream[Chunk](res, body, nil, c.unmarshal) } // ChatRequest builds the request of POST /chat. @@ -500,7 +526,7 @@ func (c *Client) ChatRequest(ctx context.Context, opts *ChatRequestOptions, edit b := httpclient.NewRequestBuilder(http.MethodPost, "/chat") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -522,7 +548,7 @@ func (c *Client) ListEvents(ctx context.Context, opts *ListEventsRequestOptions, var out string if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/event-stream", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return "", err } return out, nil @@ -538,7 +564,7 @@ func (c *Client) ListEventsStream(ctx context.Context, opts *ListEventsRequestOp if err != nil { return nil, err } - return httpclient.OpenStream[Event](res, body, nil) + return httpclient.OpenStream[Event](res, body, nil, c.unmarshal) } // ListEventsRequest builds the request of GET /events. diff --git a/examples/migration/oapi-codegen-dd/gen.go b/examples/migration/oapi-codegen-dd/gen.go index c20e91f0..ea21d648 100644 --- a/examples/migration/oapi-codegen-dd/gen.go +++ b/examples/migration/oapi-codegen-dd/gen.go @@ -6,7 +6,6 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -440,7 +439,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -452,7 +452,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 67108864, Presence: bodyPresence, } @@ -476,13 +477,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -562,7 +570,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "CreatePet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -678,7 +686,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -894,13 +902,25 @@ func WithRequestEditor(fns ...RequestEditor) PetClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) PetClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *PetClient) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // PetClient calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type PetClient struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewPetClient returns a client of the API at baseURL. @@ -910,7 +930,13 @@ func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error) { return nil, err } - c := &PetClient{baseURL: u, doer: &http.Client{}, timeout: 5 * time.Second} + c := &PetClient{ + baseURL: u, + doer: &http.Client{}, + timeout: 5 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -932,7 +958,7 @@ func (c *PetClient) ListPets(ctx context.Context, opts *ListPetsRequestOptions, if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -953,7 +979,7 @@ func (c *PetClient) ListPetsWithResponse(ctx context.Context, opts *ListPetsRequ if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -987,7 +1013,7 @@ func (c *PetClient) CreatePet(ctx context.Context, opts *CreatePetRequestOptions if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -1009,7 +1035,7 @@ func (c *PetClient) CreatePetWithResponse(ctx context.Context, opts *CreatePetRe {Status: "201", MediaType: "application/json", Dst: &out.JSON201}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, {Status: "201", IsHeaders: true, Dst: &out.Headers201}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -1023,7 +1049,7 @@ func (c *PetClient) CreatePetRequest(ctx context.Context, opts *CreatePetRequest b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -1045,7 +1071,7 @@ func (c *PetClient) GetPet(ctx context.Context, opts *GetPetRequestOptions, edit if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "404", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -1066,7 +1092,7 @@ func (c *PetClient) GetPetWithResponse(ctx context.Context, opts *GetPetRequestO if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "404", MediaType: "application/json", Dst: &out.JSON404}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -1094,7 +1120,7 @@ func (c *PetClient) DeletePet(ctx context.Context, opts *DeletePetRequestOptions if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeletePetWithResponse calls DELETE /pets/{id} and returns the whole response. diff --git a/examples/migration/oapi-codegen/gen.go b/examples/migration/oapi-codegen/gen.go index 7d6bb955..5513f364 100644 --- a/examples/migration/oapi-codegen/gen.go +++ b/examples/migration/oapi-codegen/gen.go @@ -4,8 +4,8 @@ package petstore import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -426,7 +426,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -437,7 +438,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -460,13 +462,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -530,7 +539,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -626,7 +635,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -842,13 +851,25 @@ func WithRequestEditor(fns ...RequestEditor) PetClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) PetClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *PetClient) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // PetClient calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type PetClient struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewPetClient returns a client of the API at baseURL. @@ -858,7 +879,13 @@ func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error) { return nil, err } - c := &PetClient{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &PetClient{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -880,7 +907,7 @@ func (c *PetClient) ListPets(ctx context.Context, opts *ListPetsRequestOptions, if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -901,7 +928,7 @@ func (c *PetClient) ListPetsWithResponse(ctx context.Context, opts *ListPetsRequ if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -935,7 +962,7 @@ func (c *PetClient) CreatePet(ctx context.Context, opts *CreatePetRequestOptions if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -957,7 +984,7 @@ func (c *PetClient) CreatePetWithResponse(ctx context.Context, opts *CreatePetRe {Status: "201", MediaType: "application/json", Dst: &out.JSON201}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, {Status: "201", IsHeaders: true, Dst: &out.Headers201}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -971,7 +998,7 @@ func (c *PetClient) CreatePetRequest(ctx context.Context, opts *CreatePetRequest b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -993,7 +1020,7 @@ func (c *PetClient) GetPet(ctx context.Context, opts *GetPetRequestOptions, edit if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "404", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -1014,7 +1041,7 @@ func (c *PetClient) GetPetWithResponse(ctx context.Context, opts *GetPetRequestO if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "404", MediaType: "application/json", Dst: &out.JSON404}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -1042,7 +1069,7 @@ func (c *PetClient) DeletePet(ctx context.Context, opts *DeletePetRequestOptions if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeletePetWithResponse calls DELETE /pets/{id} and returns the whole response. diff --git a/examples/migration/ogen/gen.go b/examples/migration/ogen/gen.go index 890e924b..55dfa3b7 100644 --- a/examples/migration/ogen/gen.go +++ b/examples/migration/ogen/gen.go @@ -4,8 +4,8 @@ package petstore import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -425,7 +425,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -436,7 +437,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -459,13 +461,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -529,7 +538,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -625,7 +634,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -780,13 +789,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -796,7 +817,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -818,7 +845,7 @@ func (c *Client) ListPets(ctx context.Context, opts *ListPetsRequestOptions, edi if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -852,7 +879,7 @@ func (c *Client) CreatePet(ctx context.Context, opts *CreatePetRequestOptions, e if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "201", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -866,7 +893,7 @@ func (c *Client) CreatePetRequest(ctx context.Context, opts *CreatePetRequestOpt b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -888,7 +915,7 @@ func (c *Client) GetPet(ctx context.Context, opts *GetPetRequestOptions, editors if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "404", MediaType: "application/json", Dst: new(Error)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -916,7 +943,7 @@ func (c *Client) DeletePet(ctx context.Context, opts *DeletePetRequestOptions, e if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeletePetRequest builds the request of DELETE /pets/{id}. diff --git a/examples/models/no-descriptions/gen.go b/examples/models/no-descriptions/gen.go index 54e73714..9d780630 100644 --- a/examples/models/no-descriptions/gen.go +++ b/examples/models/no-descriptions/gen.go @@ -4,6 +4,7 @@ package nodescriptions import ( "context" + "encoding/json" "net/http" "net/url" "slices" @@ -98,13 +99,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -114,7 +127,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -135,7 +154,7 @@ func (c *Client) GetPet(ctx context.Context, opts *GetPetRequestOptions, editors var out *Pet if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -165,7 +184,7 @@ func (c *Client) DeletePet(ctx context.Context, opts *DeletePetRequestOptions, e if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // DeletePetRequest builds the request of DELETE /pets/{id}. diff --git a/examples/models/nullable/gen.go b/examples/models/nullable/gen.go index e4e2c388..9b7e4cd0 100644 --- a/examples/models/nullable/gen.go +++ b/examples/models/nullable/gen.go @@ -4,8 +4,8 @@ package nullable import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" "slices" @@ -194,7 +194,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -206,7 +207,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -230,13 +232,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -291,7 +300,7 @@ func (a *HTTPAdapter) UpdatePet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "UpdatePet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "UpdatePet", err) return } @@ -341,7 +350,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -442,13 +451,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -458,7 +479,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -479,7 +506,7 @@ func (c *Client) UpdatePet(ctx context.Context, opts *UpdatePetRequestOptions, e var out *Pet if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -499,7 +526,7 @@ func (c *Client) UpdatePetRequest(ctx context.Context, opts *UpdatePetRequestOpt } switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } diff --git a/examples/server/beego/basic/gen.go b/examples/server/beego/basic/gen.go index 65abcbd7..a3d20db5 100644 --- a/examples/server/beego/basic/gen.go +++ b/examples/server/beego/basic/gen.go @@ -4,6 +4,7 @@ package basic import ( "context" + "encoding/json" "errors" "io" "net/http" @@ -376,7 +377,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -387,7 +389,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -410,13 +413,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -471,7 +481,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -570,7 +580,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/beego/bodies/gen.go b/examples/server/beego/bodies/gen.go index d5e44dd6..ebde56a3 100644 --- a/examples/server/beego/bodies/gen.go +++ b/examples/server/beego/bodies/gen.go @@ -4,6 +4,7 @@ package bodies import ( "context" + "encoding/json" "errors" "io" "iter" @@ -676,7 +677,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -687,7 +689,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -710,13 +713,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -748,7 +758,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1023,7 +1033,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/beego/bodies/gen_test.go b/examples/server/beego/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/beego/bodies/gen_test.go +++ b/examples/server/beego/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/beego/errors/gen.go b/examples/server/beego/errors/gen.go index 6d6b6b69..f1d59b9c 100644 --- a/examples/server/beego/errors/gen.go +++ b/examples/server/beego/errors/gen.go @@ -4,6 +4,7 @@ package errors import ( "context" + "encoding/json" "errors" "io" "net/http" @@ -304,7 +305,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -316,7 +318,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -340,13 +343,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -432,7 +442,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -495,7 +505,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/beego/params/gen.go b/examples/server/beego/params/gen.go index 08d05bac..6aef1642 100644 --- a/examples/server/beego/params/gen.go +++ b/examples/server/beego/params/gen.go @@ -4,6 +4,7 @@ package params import ( "context" + "encoding/json" "errors" "io" "mime/multipart" @@ -609,7 +610,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -620,7 +622,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -643,13 +646,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -867,7 +877,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/beego/paths/gen.go b/examples/server/beego/paths/gen.go index 063e9e09..d65705c1 100644 --- a/examples/server/beego/paths/gen.go +++ b/examples/server/beego/paths/gen.go @@ -4,6 +4,7 @@ package paths import ( "context" + "encoding/json" "errors" "io" "net/http" @@ -812,7 +813,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -823,7 +825,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -846,13 +849,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1162,7 +1172,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/beego/scaffolds/gen.go b/examples/server/beego/scaffolds/gen.go index 34fad29b..d348f3f3 100644 --- a/examples/server/beego/scaffolds/gen.go +++ b/examples/server/beego/scaffolds/gen.go @@ -4,6 +4,7 @@ package scaffolds import ( "context" + "encoding/json" "errors" "io" "net/http" @@ -170,7 +171,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -181,7 +183,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -204,13 +207,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -259,7 +269,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -297,7 +307,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/beego/split/api/gen.go b/examples/server/beego/split/api/gen.go index 00585862..33e5840f 100644 --- a/examples/server/beego/split/api/gen.go +++ b/examples/server/beego/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/beego/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/basic/gen.go b/examples/server/chi/basic/gen.go index 91cf635a..155495c1 100644 --- a/examples/server/chi/basic/gen.go +++ b/examples/server/chi/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -469,7 +478,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -568,7 +577,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/bodies/gen.go b/examples/server/chi/bodies/gen.go index 4e13ede7..2ea1b579 100644 --- a/examples/server/chi/bodies/gen.go +++ b/examples/server/chi/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -746,7 +755,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1021,7 +1030,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/bodies/gen_test.go b/examples/server/chi/bodies/gen_test.go index 2311243c..4c9ff013 100644 --- a/examples/server/chi/bodies/gen_test.go +++ b/examples/server/chi/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // echo answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(echo{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(echo{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(echo{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/chi/errors/gen.go b/examples/server/chi/errors/gen.go index bc9b45bf..0e7bd748 100644 --- a/examples/server/chi/errors/gen.go +++ b/examples/server/chi/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -430,7 +439,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -493,7 +502,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/params/gen.go b/examples/server/chi/params/gen.go index 7a6c3926..b42d0192 100644 --- a/examples/server/chi/params/gen.go +++ b/examples/server/chi/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -865,7 +874,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/paths/gen.go b/examples/server/chi/paths/gen.go index 9229188b..b7dd7eeb 100644 --- a/examples/server/chi/paths/gen.go +++ b/examples/server/chi/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1160,7 +1169,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/scaffolds/gen.go b/examples/server/chi/scaffolds/gen.go index 310a0d8a..c0a48945 100644 --- a/examples/server/chi/scaffolds/gen.go +++ b/examples/server/chi/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -257,7 +266,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -295,7 +304,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/chi/split/api/gen.go b/examples/server/chi/split/api/gen.go index c64a11c7..855d04da 100644 --- a/examples/server/chi/split/api/gen.go +++ b/examples/server/chi/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -90,7 +90,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -101,7 +102,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -124,13 +126,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -192,7 +201,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/contract/gen.go b/examples/server/contract/gen.go index eb761950..be05d1e5 100644 --- a/examples/server/contract/gen.go +++ b/examples/server/contract/gen.go @@ -4,8 +4,8 @@ package contract import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -453,7 +453,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -464,7 +465,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -487,13 +489,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -558,7 +567,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.BodyJSON, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -670,7 +679,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/basic/gen.go b/examples/server/echo/basic/gen.go index 5094e0d1..eab8a3a2 100644 --- a/examples/server/echo/basic/gen.go +++ b/examples/server/echo/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v4" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -472,7 +481,7 @@ func (a *HTTPAdapter) CreatePet(c echo.Context) error { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return nil } @@ -578,7 +587,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/bodies/gen.go b/examples/server/echo/bodies/gen.go index 2f38b82f..e1ef14d8 100644 --- a/examples/server/echo/bodies/gen.go +++ b/examples/server/echo/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -747,7 +756,7 @@ func (a *HTTPAdapter) PostJSON(c echo.Context) error { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return nil } @@ -1041,7 +1050,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/bodies/gen_test.go b/examples/server/echo/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/echo/bodies/gen_test.go +++ b/examples/server/echo/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/echo/errors/gen.go b/examples/server/echo/errors/gen.go index 7ed89bf5..d5bb111c 100644 --- a/examples/server/echo/errors/gen.go +++ b/examples/server/echo/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v4" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -433,7 +442,7 @@ func (a *HTTPAdapter) PutPet(c echo.Context) error { a.failBody(w, r, "PutPet", err) return nil } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return nil } @@ -497,7 +506,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/params/gen.go b/examples/server/echo/params/gen.go index e7d2eb5e..769e4030 100644 --- a/examples/server/echo/params/gen.go +++ b/examples/server/echo/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -875,7 +884,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/paths/gen.go b/examples/server/echo/paths/gen.go index 149d3971..90808d68 100644 --- a/examples/server/echo/paths/gen.go +++ b/examples/server/echo/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v4" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1184,7 +1193,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/scaffolds/gen.go b/examples/server/echo/scaffolds/gen.go index 0bca0aea..f1d4b8bb 100644 --- a/examples/server/echo/scaffolds/gen.go +++ b/examples/server/echo/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v4" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -260,7 +269,7 @@ func (a *HTTPAdapter) CreateTodo(c echo.Context) error { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return nil } @@ -299,7 +308,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echo/split/api/gen.go b/examples/server/echo/split/api/gen.go index 100f16b6..8e0c6d4d 100644 --- a/examples/server/echo/split/api/gen.go +++ b/examples/server/echo/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v4" @@ -90,7 +90,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -101,7 +102,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -124,13 +126,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -194,7 +203,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/basic/gen.go b/examples/server/echov5/basic/gen.go index d08a11c5..cb6508c6 100644 --- a/examples/server/echov5/basic/gen.go +++ b/examples/server/echov5/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v5" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -472,7 +481,7 @@ func (a *HTTPAdapter) CreatePet(c *echo.Context) error { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return nil } @@ -578,7 +587,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/bodies/gen.go b/examples/server/echov5/bodies/gen.go index 05bcdc63..e82d8137 100644 --- a/examples/server/echov5/bodies/gen.go +++ b/examples/server/echov5/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -747,7 +756,7 @@ func (a *HTTPAdapter) PostJSON(c *echo.Context) error { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return nil } @@ -1041,7 +1050,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/bodies/gen_test.go b/examples/server/echov5/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/echov5/bodies/gen_test.go +++ b/examples/server/echov5/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/echov5/errors/gen.go b/examples/server/echov5/errors/gen.go index 76082616..c1859812 100644 --- a/examples/server/echov5/errors/gen.go +++ b/examples/server/echov5/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v5" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -433,7 +442,7 @@ func (a *HTTPAdapter) PutPet(c *echo.Context) error { a.failBody(w, r, "PutPet", err) return nil } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return nil } @@ -497,7 +506,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/params/gen.go b/examples/server/echov5/params/gen.go index c9dd27c9..a5c0f416 100644 --- a/examples/server/echov5/params/gen.go +++ b/examples/server/echov5/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -875,7 +884,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/paths/gen.go b/examples/server/echov5/paths/gen.go index d761b375..86d91fcf 100644 --- a/examples/server/echov5/paths/gen.go +++ b/examples/server/echov5/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v5" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1184,7 +1193,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/scaffolds/gen.go b/examples/server/echov5/scaffolds/gen.go index d127c31d..99b4800c 100644 --- a/examples/server/echov5/scaffolds/gen.go +++ b/examples/server/echov5/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v5" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -260,7 +269,7 @@ func (a *HTTPAdapter) CreateTodo(c *echo.Context) error { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return nil } @@ -299,7 +308,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/echov5/split/api/gen.go b/examples/server/echov5/split/api/gen.go index 8bb7cab2..e20e53b3 100644 --- a/examples/server/echov5/split/api/gen.go +++ b/examples/server/echov5/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v5" @@ -90,7 +90,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -101,7 +102,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -124,13 +126,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -194,7 +203,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/basic/gen.go b/examples/server/fasthttp/basic/gen.go index 5f105ea6..14d99211 100644 --- a/examples/server/fasthttp/basic/gen.go +++ b/examples/server/fasthttp/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/fasthttp/router" @@ -376,7 +376,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -387,7 +388,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -410,13 +412,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -471,7 +480,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -570,7 +579,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/bodies/gen.go b/examples/server/fasthttp/bodies/gen.go index f1d1c2a8..5725e6bc 100644 --- a/examples/server/fasthttp/bodies/gen.go +++ b/examples/server/fasthttp/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -676,7 +676,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -687,7 +688,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -710,13 +712,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -748,7 +757,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1023,7 +1032,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/bodies/gen_test.go b/examples/server/fasthttp/bodies/gen_test.go index 11c06f41..8ea2a8f9 100644 --- a/examples/server/fasthttp/bodies/gen_test.go +++ b/examples/server/fasthttp/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -18,7 +18,6 @@ import ( "github.com/mockzilla/mockzilla-codegen/examples/server/fasthttp/internal/fasthttptest" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -98,22 +97,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, fasthttptest.Handler((NewRouter(mirror{})).Handler)) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - fasthttptest.Handler((NewRouter(mirror{}, WithJSONDecoder(strict))).Handler).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + fasthttptest.Handler((NewRouter(mirror{}, tc.opt)).Handler).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/fasthttp/errors/gen.go b/examples/server/fasthttp/errors/gen.go index e4be8a2a..04101a6b 100644 --- a/examples/server/fasthttp/errors/gen.go +++ b/examples/server/fasthttp/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/fasthttp/router" @@ -304,7 +304,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -316,7 +317,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -340,13 +342,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -432,7 +441,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -495,7 +504,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/params/gen.go b/examples/server/fasthttp/params/gen.go index 321f3d60..f85b4b3a 100644 --- a/examples/server/fasthttp/params/gen.go +++ b/examples/server/fasthttp/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -609,7 +609,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -620,7 +621,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -643,13 +645,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -867,7 +876,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/paths/gen.go b/examples/server/fasthttp/paths/gen.go index e46b7d22..44dfab9d 100644 --- a/examples/server/fasthttp/paths/gen.go +++ b/examples/server/fasthttp/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/fasthttp/router" @@ -812,7 +812,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -823,7 +824,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -846,13 +848,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1162,7 +1171,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/scaffolds/gen.go b/examples/server/fasthttp/scaffolds/gen.go index fff830dd..d79e7916 100644 --- a/examples/server/fasthttp/scaffolds/gen.go +++ b/examples/server/fasthttp/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/fasthttp/router" @@ -170,7 +170,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -181,7 +182,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -204,13 +206,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -259,7 +268,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -297,7 +306,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fasthttp/split/api/gen.go b/examples/server/fasthttp/split/api/gen.go index 1cfdf650..aa8d7019 100644 --- a/examples/server/fasthttp/split/api/gen.go +++ b/examples/server/fasthttp/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/fasthttp/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/basic/gen.go b/examples/server/fiber/basic/gen.go index 0e3c497b..ca615d1c 100644 --- a/examples/server/fiber/basic/gen.go +++ b/examples/server/fiber/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "strings" @@ -376,7 +376,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -387,7 +388,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -410,13 +412,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -471,7 +480,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -570,7 +579,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/bodies/gen.go b/examples/server/fiber/bodies/gen.go index d60c1eee..c79b0f09 100644 --- a/examples/server/fiber/bodies/gen.go +++ b/examples/server/fiber/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" "strings" @@ -676,7 +676,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -687,7 +688,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -710,13 +712,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -748,7 +757,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1023,7 +1032,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/bodies/gen_test.go b/examples/server/fiber/bodies/gen_test.go index c74fa003..e99a8661 100644 --- a/examples/server/fiber/bodies/gen_test.go +++ b/examples/server/fiber/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -18,7 +18,6 @@ import ( "github.com/gofiber/fiber/v3/middleware/adaptor" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -98,22 +97,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, adaptor.FiberApp(NewRouter(mirror{}))) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - adaptor.FiberApp(NewRouter(mirror{}, WithJSONDecoder(strict))).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + adaptor.FiberApp(NewRouter(mirror{}, tc.opt)).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/fiber/errors/gen.go b/examples/server/fiber/errors/gen.go index 67c858ca..e2bd4563 100644 --- a/examples/server/fiber/errors/gen.go +++ b/examples/server/fiber/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "strings" @@ -304,7 +304,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -316,7 +317,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -340,13 +342,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -432,7 +441,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -495,7 +504,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/params/gen.go b/examples/server/fiber/params/gen.go index 6a165c5d..af068e4e 100644 --- a/examples/server/fiber/params/gen.go +++ b/examples/server/fiber/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "strings" @@ -609,7 +609,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -620,7 +621,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -643,13 +645,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -867,7 +876,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/paths/gen.go b/examples/server/fiber/paths/gen.go index f263d9f7..1a432acf 100644 --- a/examples/server/fiber/paths/gen.go +++ b/examples/server/fiber/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "strings" @@ -812,7 +812,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -823,7 +824,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -846,13 +848,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1162,7 +1171,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/scaffolds/gen.go b/examples/server/fiber/scaffolds/gen.go index 97b74324..465bcca9 100644 --- a/examples/server/fiber/scaffolds/gen.go +++ b/examples/server/fiber/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "strings" @@ -170,7 +170,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -181,7 +182,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -204,13 +206,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -259,7 +268,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -297,7 +306,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/fiber/split/api/gen.go b/examples/server/fiber/split/api/gen.go index 5a62e460..f4c827a9 100644 --- a/examples/server/fiber/split/api/gen.go +++ b/examples/server/fiber/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/fiber/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/basic/gen.go b/examples/server/gin/basic/gen.go index 29cfa892..7632a6de 100644 --- a/examples/server/gin/basic/gen.go +++ b/examples/server/gin/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gin-gonic/gin" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -469,7 +478,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -568,7 +577,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/bodies/gen.go b/examples/server/gin/bodies/gen.go index d11e4670..e85f9f4a 100644 --- a/examples/server/gin/bodies/gen.go +++ b/examples/server/gin/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -746,7 +755,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1021,7 +1030,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/bodies/gen_test.go b/examples/server/gin/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/gin/bodies/gen_test.go +++ b/examples/server/gin/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/gin/errors/gen.go b/examples/server/gin/errors/gen.go index e656edb0..ab5fe795 100644 --- a/examples/server/gin/errors/gen.go +++ b/examples/server/gin/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gin-gonic/gin" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -430,7 +439,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -493,7 +502,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/params/gen.go b/examples/server/gin/params/gen.go index bdfecf39..42887143 100644 --- a/examples/server/gin/params/gen.go +++ b/examples/server/gin/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -865,7 +874,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/paths/gen.go b/examples/server/gin/paths/gen.go index fa86f4ae..b3e973ef 100644 --- a/examples/server/gin/paths/gen.go +++ b/examples/server/gin/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gin-gonic/gin" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1160,7 +1169,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/scaffolds/gen.go b/examples/server/gin/scaffolds/gen.go index 09c5ff35..fabb3cf8 100644 --- a/examples/server/gin/scaffolds/gen.go +++ b/examples/server/gin/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gin-gonic/gin" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -257,7 +266,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -295,7 +304,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gin/split/api/gen.go b/examples/server/gin/split/api/gen.go index b2e08c11..aef52180 100644 --- a/examples/server/gin/split/api/gen.go +++ b/examples/server/gin/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/gin/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/basic/gen.go b/examples/server/goframe/basic/gen.go index d79af04a..3251b965 100644 --- a/examples/server/goframe/basic/gen.go +++ b/examples/server/goframe/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" @@ -377,7 +377,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -388,7 +389,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -411,13 +413,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -472,7 +481,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -571,7 +580,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/bodies/gen.go b/examples/server/goframe/bodies/gen.go index cd3890a9..2da901ce 100644 --- a/examples/server/goframe/bodies/gen.go +++ b/examples/server/goframe/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" "net/url" @@ -677,7 +677,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -688,7 +689,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -711,13 +713,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -749,7 +758,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1024,7 +1033,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/bodies/gen_test.go b/examples/server/goframe/bodies/gen_test.go index 32f5328d..141f2827 100644 --- a/examples/server/goframe/bodies/gen_test.go +++ b/examples/server/goframe/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -18,7 +18,6 @@ import ( "github.com/mockzilla/mockzilla-codegen/examples/server/goframe/internal/goframetest" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -98,22 +97,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, goframetest.Handler(t, NewRouter(mirror{}))) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - goframetest.Handler(t, NewRouter(mirror{}, WithJSONDecoder(strict))).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + goframetest.Handler(t, NewRouter(mirror{}, tc.opt)).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/goframe/errors/gen.go b/examples/server/goframe/errors/gen.go index e1db8efd..27e20f16 100644 --- a/examples/server/goframe/errors/gen.go +++ b/examples/server/goframe/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" @@ -305,7 +305,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -317,7 +318,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -341,13 +343,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -433,7 +442,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -496,7 +505,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/params/gen.go b/examples/server/goframe/params/gen.go index f21827a0..c7c44206 100644 --- a/examples/server/goframe/params/gen.go +++ b/examples/server/goframe/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "net/url" @@ -610,7 +610,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -621,7 +622,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -644,13 +646,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -868,7 +877,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/paths/gen.go b/examples/server/goframe/paths/gen.go index 743e915f..1b689fe7 100644 --- a/examples/server/goframe/paths/gen.go +++ b/examples/server/goframe/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" @@ -813,7 +813,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -824,7 +825,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -847,13 +849,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1163,7 +1172,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/scaffolds/gen.go b/examples/server/goframe/scaffolds/gen.go index 39126f82..b731c77f 100644 --- a/examples/server/goframe/scaffolds/gen.go +++ b/examples/server/goframe/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "net/url" @@ -171,7 +171,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -182,7 +183,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -205,13 +207,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -260,7 +269,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -298,7 +307,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/goframe/split/api/gen.go b/examples/server/goframe/split/api/gen.go index dae8e3c8..de913e1c 100644 --- a/examples/server/goframe/split/api/gen.go +++ b/examples/server/goframe/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/goframe/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/basic/gen.go b/examples/server/gorillamux/basic/gen.go index 2d84f37d..b9736fd0 100644 --- a/examples/server/gorillamux/basic/gen.go +++ b/examples/server/gorillamux/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gorilla/mux" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -469,7 +478,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -568,7 +577,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/bodies/gen.go b/examples/server/gorillamux/bodies/gen.go index e2786493..72baeb2f 100644 --- a/examples/server/gorillamux/bodies/gen.go +++ b/examples/server/gorillamux/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -746,7 +755,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1021,7 +1030,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/bodies/gen_test.go b/examples/server/gorillamux/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/gorillamux/bodies/gen_test.go +++ b/examples/server/gorillamux/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/gorillamux/errors/gen.go b/examples/server/gorillamux/errors/gen.go index b282562e..6db0a7e1 100644 --- a/examples/server/gorillamux/errors/gen.go +++ b/examples/server/gorillamux/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gorilla/mux" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -430,7 +439,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -493,7 +502,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/params/gen.go b/examples/server/gorillamux/params/gen.go index 35831338..f7fabccf 100644 --- a/examples/server/gorillamux/params/gen.go +++ b/examples/server/gorillamux/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -865,7 +874,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/paths/gen.go b/examples/server/gorillamux/paths/gen.go index 64c45755..ec01753f 100644 --- a/examples/server/gorillamux/paths/gen.go +++ b/examples/server/gorillamux/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gorilla/mux" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1160,7 +1169,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/scaffolds/gen.go b/examples/server/gorillamux/scaffolds/gen.go index 4701d151..5be9bbe8 100644 --- a/examples/server/gorillamux/scaffolds/gen.go +++ b/examples/server/gorillamux/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gorilla/mux" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -257,7 +266,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -295,7 +304,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gorillamux/split/api/gen.go b/examples/server/gorillamux/split/api/gen.go index af0f28ee..dd045846 100644 --- a/examples/server/gorillamux/split/api/gen.go +++ b/examples/server/gorillamux/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/gorilla/mux" @@ -90,7 +90,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -101,7 +102,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -124,13 +126,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -192,7 +201,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/basic/gen.go b/examples/server/gozero/basic/gen.go index d71f8c21..22b69d9c 100644 --- a/examples/server/gozero/basic/gen.go +++ b/examples/server/gozero/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -376,7 +376,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -387,7 +388,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -410,13 +412,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -471,7 +480,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -570,7 +579,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/bodies/gen.go b/examples/server/gozero/bodies/gen.go index 45d1e24c..e94517ec 100644 --- a/examples/server/gozero/bodies/gen.go +++ b/examples/server/gozero/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -675,7 +675,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -686,7 +687,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -709,13 +711,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -747,7 +756,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1022,7 +1031,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/bodies/gen_test.go b/examples/server/gozero/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/gozero/bodies/gen_test.go +++ b/examples/server/gozero/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/gozero/errors/gen.go b/examples/server/gozero/errors/gen.go index 916302c8..afb5cb98 100644 --- a/examples/server/gozero/errors/gen.go +++ b/examples/server/gozero/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -304,7 +304,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -316,7 +317,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -340,13 +342,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -432,7 +441,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -495,7 +504,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/params/gen.go b/examples/server/gozero/params/gen.go index 8f0ec820..7138fa54 100644 --- a/examples/server/gozero/params/gen.go +++ b/examples/server/gozero/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -609,7 +609,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -620,7 +621,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -643,13 +645,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -867,7 +876,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/paths/gen.go b/examples/server/gozero/paths/gen.go index 82ee8b55..954e658a 100644 --- a/examples/server/gozero/paths/gen.go +++ b/examples/server/gozero/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -812,7 +812,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -823,7 +824,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -846,13 +848,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1162,7 +1171,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/scaffolds/gen.go b/examples/server/gozero/scaffolds/gen.go index 1f2bb9dc..9bab056f 100644 --- a/examples/server/gozero/scaffolds/gen.go +++ b/examples/server/gozero/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -169,7 +169,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -180,7 +181,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -203,13 +205,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -258,7 +267,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -296,7 +305,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/gozero/split/api/gen.go b/examples/server/gozero/split/api/gen.go index 38812286..e3446643 100644 --- a/examples/server/gozero/split/api/gen.go +++ b/examples/server/gozero/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/gozero/split/models" @@ -90,7 +90,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -101,7 +102,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -124,13 +126,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -192,7 +201,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/basic/gen.go b/examples/server/hertz/basic/gen.go index 9c050a21..0a93ba09 100644 --- a/examples/server/hertz/basic/gen.go +++ b/examples/server/hertz/basic/gen.go @@ -5,8 +5,8 @@ package basic import ( "bytes" "context" + "encoding/json" "errors" - "io" "net/http" "github.com/cloudwego/hertz/pkg/app" @@ -378,7 +378,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -389,7 +390,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -412,13 +414,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -473,7 +482,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -572,7 +581,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/bodies/gen.go b/examples/server/hertz/bodies/gen.go index a7c47a8a..ca8b98dd 100644 --- a/examples/server/hertz/bodies/gen.go +++ b/examples/server/hertz/bodies/gen.go @@ -5,8 +5,8 @@ package bodies import ( "bytes" "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -678,7 +678,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -689,7 +690,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -712,13 +714,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -750,7 +759,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1025,7 +1034,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/bodies/gen_test.go b/examples/server/hertz/bodies/gen_test.go index 0a79c3ae..a1f9c50d 100644 --- a/examples/server/hertz/bodies/gen_test.go +++ b/examples/server/hertz/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -18,7 +18,6 @@ import ( "github.com/mockzilla/mockzilla-codegen/examples/server/hertz/internal/hertztest" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -98,22 +97,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, hertztest.Handler((NewRouter(mirror{})).Engine)) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - hertztest.Handler((NewRouter(mirror{}, WithJSONDecoder(strict))).Engine).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + hertztest.Handler((NewRouter(mirror{}, tc.opt)).Engine).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/hertz/errors/gen.go b/examples/server/hertz/errors/gen.go index 85843429..dd6f4c99 100644 --- a/examples/server/hertz/errors/gen.go +++ b/examples/server/hertz/errors/gen.go @@ -5,8 +5,8 @@ package errors import ( "bytes" "context" + "encoding/json" "errors" - "io" "net/http" "github.com/cloudwego/hertz/pkg/app" @@ -306,7 +306,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -318,7 +319,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -342,13 +344,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -434,7 +443,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -497,7 +506,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/params/gen.go b/examples/server/hertz/params/gen.go index 13c622db..e5482643 100644 --- a/examples/server/hertz/params/gen.go +++ b/examples/server/hertz/params/gen.go @@ -5,8 +5,8 @@ package params import ( "bytes" "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -611,7 +611,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -622,7 +623,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -645,13 +647,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -869,7 +878,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/paths/gen.go b/examples/server/hertz/paths/gen.go index 94e1a11e..5b681dd5 100644 --- a/examples/server/hertz/paths/gen.go +++ b/examples/server/hertz/paths/gen.go @@ -5,8 +5,8 @@ package paths import ( "bytes" "context" + "encoding/json" "errors" - "io" "net/http" "github.com/cloudwego/hertz/pkg/app" @@ -814,7 +814,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -825,7 +826,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -848,13 +850,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1164,7 +1173,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/scaffolds/gen.go b/examples/server/hertz/scaffolds/gen.go index 2d156418..d768a691 100644 --- a/examples/server/hertz/scaffolds/gen.go +++ b/examples/server/hertz/scaffolds/gen.go @@ -5,8 +5,8 @@ package scaffolds import ( "bytes" "context" + "encoding/json" "errors" - "io" "net/http" "github.com/cloudwego/hertz/pkg/app" @@ -172,7 +172,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -183,7 +184,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -206,13 +208,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -261,7 +270,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -299,7 +308,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/hertz/split/api/gen.go b/examples/server/hertz/split/api/gen.go index c493fcb4..dca4c0cb 100644 --- a/examples/server/hertz/split/api/gen.go +++ b/examples/server/hertz/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/hertz/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/basic/gen.go b/examples/server/iris/basic/gen.go index 87d58cc9..e26710a4 100644 --- a/examples/server/iris/basic/gen.go +++ b/examples/server/iris/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" iris "github.com/kataras/iris/v12" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -469,7 +478,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -568,7 +577,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/bodies/gen.go b/examples/server/iris/bodies/gen.go index ce83a900..e6719bc0 100644 --- a/examples/server/iris/bodies/gen.go +++ b/examples/server/iris/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -746,7 +755,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1021,7 +1030,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/bodies/gen_test.go b/examples/server/iris/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/iris/bodies/gen_test.go +++ b/examples/server/iris/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/iris/errors/gen.go b/examples/server/iris/errors/gen.go index 07fa7325..905a2166 100644 --- a/examples/server/iris/errors/gen.go +++ b/examples/server/iris/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" iris "github.com/kataras/iris/v12" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -430,7 +439,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -493,7 +502,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/params/gen.go b/examples/server/iris/params/gen.go index 95cac4d2..dec87a0b 100644 --- a/examples/server/iris/params/gen.go +++ b/examples/server/iris/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -865,7 +874,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/paths/gen.go b/examples/server/iris/paths/gen.go index 2d7a96bc..1ef1f373 100644 --- a/examples/server/iris/paths/gen.go +++ b/examples/server/iris/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" iris "github.com/kataras/iris/v12" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1160,7 +1169,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/scaffolds/gen.go b/examples/server/iris/scaffolds/gen.go index 0d473b13..9eb353c0 100644 --- a/examples/server/iris/scaffolds/gen.go +++ b/examples/server/iris/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" iris "github.com/kataras/iris/v12" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -257,7 +266,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -295,7 +304,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/iris/split/api/gen.go b/examples/server/iris/split/api/gen.go index c4288316..05d8a0bf 100644 --- a/examples/server/iris/split/api/gen.go +++ b/examples/server/iris/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/iris/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/basic/gen.go b/examples/server/kratos/basic/gen.go index 42c25873..8d686e99 100644 --- a/examples/server/kratos/basic/gen.go +++ b/examples/server/kratos/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" khttp "github.com/go-kratos/kratos/v2/transport/http" @@ -374,7 +374,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -385,7 +386,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -408,13 +410,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -472,7 +481,7 @@ func (a *HTTPAdapter) CreatePet(c khttp.Context) error { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return nil } @@ -578,7 +587,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/bodies/gen.go b/examples/server/kratos/bodies/gen.go index cd8b9c52..772b8577 100644 --- a/examples/server/kratos/bodies/gen.go +++ b/examples/server/kratos/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -674,7 +674,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -685,7 +686,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -708,13 +710,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -747,7 +756,7 @@ func (a *HTTPAdapter) PostJSON(c khttp.Context) error { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return nil } @@ -1041,7 +1050,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/bodies/gen_test.go b/examples/server/kratos/bodies/gen_test.go index efae9500..35953ec4 100644 --- a/examples/server/kratos/bodies/gen_test.go +++ b/examples/server/kratos/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // mirror answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(mirror{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(mirror{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(mirror{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/kratos/errors/gen.go b/examples/server/kratos/errors/gen.go index 72dc8ef0..5e74259b 100644 --- a/examples/server/kratos/errors/gen.go +++ b/examples/server/kratos/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" khttp "github.com/go-kratos/kratos/v2/transport/http" @@ -302,7 +302,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -314,7 +315,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -338,13 +340,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -433,7 +442,7 @@ func (a *HTTPAdapter) PutPet(c khttp.Context) error { a.failBody(w, r, "PutPet", err) return nil } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return nil } @@ -497,7 +506,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/params/gen.go b/examples/server/kratos/params/gen.go index a2dc547e..38d90cab 100644 --- a/examples/server/kratos/params/gen.go +++ b/examples/server/kratos/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -607,7 +607,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -618,7 +619,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -641,13 +643,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -875,7 +884,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/paths/gen.go b/examples/server/kratos/paths/gen.go index 86d674a5..0b350b4a 100644 --- a/examples/server/kratos/paths/gen.go +++ b/examples/server/kratos/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" khttp "github.com/go-kratos/kratos/v2/transport/http" @@ -810,7 +810,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -821,7 +822,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -844,13 +846,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1184,7 +1193,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/scaffolds/gen.go b/examples/server/kratos/scaffolds/gen.go index ec8ed361..a8ae5d0a 100644 --- a/examples/server/kratos/scaffolds/gen.go +++ b/examples/server/kratos/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" khttp "github.com/go-kratos/kratos/v2/transport/http" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -260,7 +269,7 @@ func (a *HTTPAdapter) CreateTodo(c khttp.Context) error { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return nil } @@ -299,7 +308,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/kratos/split/api/gen.go b/examples/server/kratos/split/api/gen.go index 1d33c162..d4e03dfc 100644 --- a/examples/server/kratos/split/api/gen.go +++ b/examples/server/kratos/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" khttp "github.com/go-kratos/kratos/v2/transport/http" @@ -90,7 +90,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -101,7 +102,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -124,13 +126,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -194,7 +203,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/basic/gen.go b/examples/server/stdhttp/basic/gen.go index 24f0ad52..40feaf01 100644 --- a/examples/server/stdhttp/basic/gen.go +++ b/examples/server/stdhttp/basic/gen.go @@ -4,8 +4,8 @@ package basic import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -373,7 +373,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -384,7 +385,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -407,13 +409,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -468,7 +477,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -567,7 +576,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/bodies/gen.go b/examples/server/stdhttp/bodies/gen.go index 917babba..3ccc80a3 100644 --- a/examples/server/stdhttp/bodies/gen.go +++ b/examples/server/stdhttp/bodies/gen.go @@ -4,8 +4,8 @@ package bodies import ( "context" + "encoding/json" "errors" - "io" "iter" "net/http" @@ -673,7 +673,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -684,7 +685,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 1048576, } for _, opt := range opts { @@ -707,13 +709,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -745,7 +754,7 @@ func (a *HTTPAdapter) PostJSON(w http.ResponseWriter, r *http.Request) { opts := &PostJSONServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PostJSON", err) return } @@ -1020,7 +1029,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/bodies/gen_test.go b/examples/server/stdhttp/bodies/gen_test.go index 2311243c..4c9ff013 100644 --- a/examples/server/stdhttp/bodies/gen_test.go +++ b/examples/server/stdhttp/bodies/gen_test.go @@ -8,7 +8,7 @@ package bodies import ( "bytes" "context" - "io" + "encoding/json" "net/http/httptest" "slices" "strings" @@ -17,7 +17,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/mockzilla/mockzilla-codegen/examples/server/internal/servertest" - "github.com/mockzilla/mockzilla-codegen/pkg/runtime" ) // echo answers with what it received. @@ -97,22 +96,36 @@ func TestMultipart(t *testing.T) { servertest.Multipart(t, NewRouter(echo{})) } -func TestJSONDecoderOption(t *testing.T) { +func TestJSONOptions(t *testing.T) { t.Parallel() - strict := func(body io.Reader, dst any, isRequired bool) error { - data, err := io.ReadAll(body) - if err != nil { - return err - } - return runtime.DecodeJSON(bytes.NewReader(bytes.ToUpper(data)), dst, isRequired) + upper := func(data []byte, v any) error { + return json.Unmarshal(bytes.ToUpper(data), v) } - req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) - req.Header.Set("Content-Type", "application/json") - rec := httptest.NewRecorder() + indent := func(v any) ([]byte, error) { + return json.MarshalIndent(v, "", " ") + } + tests := []struct { + name string + opt ServerOption + wantBody string + }{ + {name: "A decoder reads the request", opt: WithJSONDecoder(upper), wantBody: `{"text":"HI"}`}, + {name: "An encoder writes the response", opt: WithJSONEncoder(indent), wantBody: "{\n \"text\": \"hi\"\n}"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - NewRouter(echo{}, WithJSONDecoder(strict)).ServeHTTP(rec, req) + req := httptest.NewRequest("POST", "/json", strings.NewReader(`{"text":"hi"}`)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() - assert.Equal(t, 200, rec.Code) - assert.Equal(t, `{"text":"HI"}`, rec.Body.String()) + NewRouter(echo{}, tc.opt).ServeHTTP(rec, req) + + assert.Equal(t, 200, rec.Code) + assert.Equal(t, tc.wantBody, rec.Body.String()) + }) + } } diff --git a/examples/server/stdhttp/errors/gen.go b/examples/server/stdhttp/errors/gen.go index 9a663517..27f4b1e9 100644 --- a/examples/server/stdhttp/errors/gen.go +++ b/examples/server/stdhttp/errors/gen.go @@ -4,8 +4,8 @@ package errors import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -301,7 +301,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 Presence runtime.PresenceChecker } @@ -313,7 +314,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, Presence: bodyPresence, } @@ -337,13 +339,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -429,7 +438,7 @@ func (a *HTTPAdapter) PutPet(w http.ResponseWriter, r *http.Request) { a.failBody(w, r, "PutPet", err) return } - if err = a.opts.JSONDecoder(body, &opts.Body, true); err != nil { + if err = runtime.DecodeJSON(body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "PutPet", err) return } @@ -492,7 +501,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/params/gen.go b/examples/server/stdhttp/params/gen.go index 807e7861..d28f31a9 100644 --- a/examples/server/stdhttp/params/gen.go +++ b/examples/server/stdhttp/params/gen.go @@ -4,8 +4,8 @@ package params import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "time" @@ -606,7 +606,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -617,7 +618,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -640,13 +642,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -864,7 +873,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/paths/gen.go b/examples/server/stdhttp/paths/gen.go index 631cc6b8..541f5e25 100644 --- a/examples/server/stdhttp/paths/gen.go +++ b/examples/server/stdhttp/paths/gen.go @@ -4,8 +4,8 @@ package paths import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -809,7 +809,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -820,7 +821,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -843,13 +845,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -1159,7 +1168,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/routes/gen.go b/examples/server/stdhttp/routes/gen.go index d500d454..2e4110ed 100644 --- a/examples/server/stdhttp/routes/gen.go +++ b/examples/server/stdhttp/routes/gen.go @@ -4,8 +4,8 @@ package routes import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -356,7 +356,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -367,7 +368,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -390,13 +392,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -428,7 +437,7 @@ func (a *HTTPAdapter) Search(w http.ResponseWriter, r *http.Request) { opts := &SearchServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Search", err) return } @@ -539,7 +548,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/scaffolds/gen.go b/examples/server/stdhttp/scaffolds/gen.go index 724965b1..b502fd95 100644 --- a/examples/server/stdhttp/scaffolds/gen.go +++ b/examples/server/stdhttp/scaffolds/gen.go @@ -4,8 +4,8 @@ package scaffolds import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/pkg/runtime" @@ -167,7 +167,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -178,7 +179,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -201,13 +203,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -256,7 +265,7 @@ func (a *HTTPAdapter) CreateTodo(w http.ResponseWriter, r *http.Request) { opts := &CreateTodoServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreateTodo", err) return } @@ -294,7 +303,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/server/stdhttp/split/api/gen.go b/examples/server/stdhttp/split/api/gen.go index 83b45868..cac0818c 100644 --- a/examples/server/stdhttp/split/api/gen.go +++ b/examples/server/stdhttp/split/api/gen.go @@ -4,8 +4,8 @@ package api import ( "context" + "encoding/json" "errors" - "io" "net/http" "github.com/mockzilla/mockzilla-codegen/examples/server/stdhttp/split/models" @@ -89,7 +89,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -100,7 +101,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -123,13 +125,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -191,7 +200,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/templates/blocks/gen.go b/examples/templates/blocks/gen.go index a778fa7e..32de2825 100644 --- a/examples/templates/blocks/gen.go +++ b/examples/templates/blocks/gen.go @@ -4,8 +4,8 @@ package blocks import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -168,7 +168,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -179,7 +180,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -202,13 +204,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -257,7 +266,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -295,7 +304,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/templates/wrapper/gen.go b/examples/templates/wrapper/gen.go index c76de82d..3fe6bb74 100644 --- a/examples/templates/wrapper/gen.go +++ b/examples/templates/wrapper/gen.go @@ -4,8 +4,8 @@ package wrapper import ( "context" + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -299,7 +299,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -310,7 +311,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -333,13 +335,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -388,7 +397,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -465,7 +474,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/examples/unions/server/gen.go b/examples/unions/server/gen.go index b0a5833d..d964b204 100644 --- a/examples/unions/server/gen.go +++ b/examples/unions/server/gen.go @@ -4,8 +4,8 @@ package server import ( "context" + "encoding/json" "errors" - "io" "mime/multipart" "net/http" "net/url" @@ -498,7 +498,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -509,7 +510,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 33554432, } for _, opt := range opts { @@ -532,13 +534,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -680,7 +689,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) @@ -820,13 +829,25 @@ func WithRequestEditor(fns ...RequestEditor) ClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) ClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *Client) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // Client calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type Client struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewClient returns a client of the API at baseURL. @@ -836,7 +857,13 @@ func NewClient(baseURL string, opts ...ClientOption) (*Client, error) { return nil, err } - c := &Client{baseURL: u, doer: &http.Client{}, timeout: 3 * time.Second} + c := &Client{ + baseURL: u, + doer: &http.Client{}, + timeout: 3 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } @@ -858,7 +885,7 @@ func (c *Client) PostForm(ctx context.Context, opts *PostFormRequestOptions, edi if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(Fault)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -893,7 +920,7 @@ func (c *Client) PostMultipart(ctx context.Context, opts *PostMultipartRequestOp var out *Shape if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -928,7 +955,7 @@ func (c *Client) PostAttachment(ctx context.Context, opts *PostAttachmentRequest var out *PostAttachmentResponse200 if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil diff --git a/internal/gen/client/client_test.go b/internal/gen/client/client_test.go index ee61bbec..3fbc636f 100644 --- a/internal/gen/client/client_test.go +++ b/internal/gen/client/client_test.go @@ -278,7 +278,7 @@ func TestViewWithoutOperations(t *testing.T) { core := string(f.render(t, PartCore)) assert.Contains(t, core, "func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error)") - assert.Contains(t, core, "c := &PetClient{baseURL: u, doer: &http.Client{}}\n") + assert.Contains(t, core, "c := &PetClient{\n\t\tbaseURL: u,\n\t\tdoer: &http.Client{},\n\t\tmarshal: json.Marshal,\n\t\tunmarshal: json.Unmarshal,\n\t}\n") assert.Contains(t, core, "// WithTimeout sets how long a call may take, 0 for no limit.\nfunc WithTimeout(d time.Duration) PetClientOption {\n") assert.Contains(t, core, "\ntype PetClientInterface interface {\n}\n\nvar _ PetClientInterface = (*PetClient)(nil)\n") assert.Equal(t, "package types\n", string(f.render(t, PartOptions))) diff --git a/internal/gen/client/core.go b/internal/gen/client/core.go index 46169004..fa06ca39 100644 --- a/internal/gen/client/core.go +++ b/internal/gen/client/core.go @@ -29,6 +29,7 @@ type CoreView struct { HTTP string Time string URL string + JSON string HTTPClient string Timeout string HasStreams bool @@ -67,6 +68,7 @@ func coreView(g *Generator, s *gocode.Scope) *CoreView { HTTP: s.Import(gomodel.Import{Path: "net/http"}), Time: s.Import(gomodel.Import{Path: "time"}), URL: s.Import(gomodel.Import{Path: "net/url"}), + JSON: s.Import(gomodel.Import{Path: "encoding/json"}), HTTPClient: s.Import(gomodel.Import{Path: gomodel.HTTPClientPath}), HasStreams: g.opts.HasStreams && slices.ContainsFunc(g.ops, hasStream), HasEnvelopes: g.opts.HasEnvelopes, diff --git a/internal/gen/client/core_test.go b/internal/gen/client/core_test.go index d92a3e18..7efb4ea5 100644 --- a/internal/gen/client/core_test.go +++ b/internal/gen/client/core_test.go @@ -54,6 +54,7 @@ func TestCoreView(t *testing.T) { HTTP: "http", Time: "time", URL: "url", + JSON: "json", HTTPClient: "httpclient", Timeout: tc.wantTimeout, HasStreams: tc.wantHasStreams, diff --git a/internal/gen/client/operations.go b/internal/gen/client/operations.go index 63e2e367..baa3df25 100644 --- a/internal/gen/client/operations.go +++ b/internal/gen/client/operations.go @@ -133,14 +133,16 @@ type ParamView struct { // BodyView is one body field: the expression that says it is set, the builder method that sends // it with the expression of its value, and the quoted media type the method takes, empty for one -// that needs none. Encoding is the runtime.Encoding a form or multipart body is sent with. An -// empty Encoder is a body the client cannot send, whose media type it reports. +// that needs none. Encoding is the runtime.Encoding a form or multipart body is sent with; IsJSON +// marks a JSON body, sent with the client's marshal function. An empty Encoder is a body the +// client cannot send, whose media type it reports. type BodyView struct { IsSet string Encoder string Value string MediaType string Encoding string + IsJSON bool } // TargetView is one httpclient.ResponseTarget: the status and media type, and the address of what @@ -289,6 +291,8 @@ func bodyView(c gomodel.Content, field string, s *gocode.Scope) BodyView { base := gomodel.Elem(t) switch v.Encoder { + case encodeJSON: + v.IsJSON = true case encodeForm, encodeMultipart: v.MediaType, v.Encoding = "", operation.Encoding(c, s.Import(gomodel.Import{Path: gomodel.RuntimePath})) case encodeFile: diff --git a/internal/gen/client/operations_test.go b/internal/gen/client/operations_test.go index f1abda3b..3a2459f0 100644 --- a/internal/gen/client/operations_test.go +++ b/internal/gen/client/operations_test.go @@ -86,9 +86,9 @@ func TestBodyView(t *testing.T) { content gomodel.Content want BodyView }{ - {name: "JSON", content: gomodel.Content{MediaType: "application/vnd.pet+json", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/vnd.pet+json"`}}, - {name: "JSON without a schema", content: gomodel.Content{MediaType: "application/json"}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/json"`}}, - {name: "JSON with parameters goes under them", content: gomodel.Content{MediaType: "application/json; charset=utf-8", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/json; charset=utf-8"`}}, + {name: "JSON", content: gomodel.Content{MediaType: "application/vnd.pet+json", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/vnd.pet+json"`, IsJSON: true}}, + {name: "JSON without a schema", content: gomodel.Content{MediaType: "application/json"}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/json"`, IsJSON: true}}, + {name: "JSON with parameters goes under them", content: gomodel.Content{MediaType: "application/json; charset=utf-8", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/json; charset=utf-8"`, IsJSON: true}}, {name: "A form", content: gomodel.Content{MediaType: "application/x-www-form-urlencoded", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "FormBody", Value: "opts.Body", Encoding: "nil"}}, {name: "A form in another case", content: gomodel.Content{MediaType: "Application/X-WWW-Form-Urlencoded; charset=utf-8", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "FormBody", Value: "opts.Body", Encoding: "nil"}}, {name: "Multipart into a struct", content: gomodel.Content{MediaType: "multipart/form-data", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "MultipartBody", Value: "opts.Body", Encoding: "nil"}}, @@ -101,7 +101,7 @@ func TestBodyView(t *testing.T) { {name: "A defined string is converted", content: gomodel.Content{MediaType: "text/markdown", Type: note}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "TextBody", Value: "string(*opts.Body)", MediaType: `"text/markdown"`}}, {name: "Bytes without a schema", content: gomodel.Content{MediaType: "image/png"}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "BytesBody", Value: "opts.Body", MediaType: `"image/png"`}}, {name: "Defined bytes are converted", content: gomodel.Content{MediaType: "image/png", Type: blob}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "BytesBody", Value: "[]byte(opts.Body)", MediaType: `"image/png"`}}, - {name: "A wildcard sends a struct as JSON", content: gomodel.Content{MediaType: "*/*", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/json"`}}, + {name: "A wildcard sends a struct as JSON", content: gomodel.Content{MediaType: "*/*", Type: pet}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "JSONBody", Value: "opts.Body", MediaType: `"application/json"`, IsJSON: true}}, {name: "A wildcard sends text as text", content: gomodel.Content{MediaType: "text/*", Type: str}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "TextBody", Value: "*opts.Body", MediaType: `"text/plain"`}}, {name: "A wildcard sends bytes as bytes", content: gomodel.Content{MediaType: "*/*"}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "BytesBody", Value: "opts.Body", MediaType: `"application/octet-stream"`}}, {name: "A wildcard streams a file under its own type", content: gomodel.Content{MediaType: "*/*", Type: fileType}, want: BodyView{IsSet: "opts.Body != nil", Encoder: "FileBody", Value: "*opts.Body", MediaType: `""`}}, diff --git a/internal/gen/client/templates/core.tmpl b/internal/gen/client/templates/core.tmpl index b970d810..2dbedd43 100644 --- a/internal/gen/client/templates/core.tmpl +++ b/internal/gen/client/templates/core.tmpl @@ -60,13 +60,25 @@ func WithRequestEditor(fns ...RequestEditor) {{.Option}} { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) {{.Option}} { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *{{.Name}}) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // {{.Name}} calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *{{.HTTPClient}}.APIError. type {{.Name}} struct { - baseURL *{{.URL}}.URL - doer HTTPDoer - timeout {{.Time}}.Duration - editors []RequestEditor + baseURL *{{.URL}}.URL + doer HTTPDoer + timeout {{.Time}}.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // New{{.Name}} returns a client of the API at baseURL. @@ -76,7 +88,15 @@ func New{{.Name}}(baseURL string, opts ...{{.Option}}) (*{{.Name}}, error) { return nil, err } - c := &{{.Name}}{baseURL: u, doer: &{{.HTTP}}.Client{}{{with .Timeout}}, timeout: {{.}}{{end}}} + c := &{{.Name}}{ + baseURL: u, + doer: &{{.HTTP}}.Client{}, +{{- with .Timeout}} + timeout: {{.}}, +{{- end}} + marshal: {{.JSON}}.Marshal, + unmarshal: {{.JSON}}.Unmarshal, + } for _, opt := range opts { opt(c) } diff --git a/internal/gen/client/templates/operations.tmpl b/internal/gen/client/templates/operations.tmpl index f30b6236..c42eaf6e 100644 --- a/internal/gen/client/templates/operations.tmpl +++ b/internal/gen/client/templates/operations.tmpl @@ -15,16 +15,16 @@ func (c *{{$.Client}}) {{.Name}}(ctx {{$.Context}}.Context, opts *{{.Options}}, var out {{.Result}} if err = {{$.HTTPClient}}.DecodeSuccess(res, body, []{{$.HTTPClient}}.ResponseTarget{ {{- template "client/targets.tmpl" .Targets}} - }); err != nil { + }, c.unmarshal); err != nil { return {{.Zero}}, err } return out, nil {{- else if .Targets}} return {{$.HTTPClient}}.DecodeSuccess(res, body, []{{$.HTTPClient}}.ResponseTarget{ {{- template "client/targets.tmpl" .Targets}} - }) + }, c.unmarshal) {{- else}} - return {{$.HTTPClient}}.DecodeSuccess(res, body, nil) + return {{$.HTTPClient}}.DecodeSuccess(res, body, nil, c.unmarshal) {{- end}} } {{- if $.HasEnvelopes}} @@ -44,7 +44,7 @@ func (c *{{$.Client}}) {{.Name}}WithResponse(ctx {{$.Context}}.Context, opts *{{ {{- if .EnvelopeTargets}} if err = {{$.HTTPClient}}.DecodeResponse(res, body, []{{$.HTTPClient}}.ResponseTarget{ {{- template "client/targets.tmpl" .EnvelopeTargets}} - }); err != nil { + }, c.unmarshal); err != nil { return out, err } {{- end}} @@ -66,9 +66,9 @@ func (c *{{$.Client}}) {{.Name}}Stream(ctx {{$.Context}}.Context, opts *{{.Optio {{- if .Stream.Targets}} return {{$.HTTPClient}}.OpenStream[{{.Stream.Frame}}](res, body, []{{$.HTTPClient}}.ResponseTarget{ {{- template "client/targets.tmpl" .Stream.Targets}} - }) + }, c.unmarshal) {{- else}} - return {{$.HTTPClient}}.OpenStream[{{.Stream.Frame}}](res, body, nil) + return {{$.HTTPClient}}.OpenStream[{{.Stream.Frame}}](res, body, nil, c.unmarshal) {{- end}} } {{- if $.HasEnvelopes}} @@ -88,13 +88,14 @@ func (c *{{$.Client}}) {{.Name}}StreamWithResponse(ctx {{$.Context}}.Context, op {{- if .EnvelopeTargets}} out.{{.Stream.Field}}, err = {{$.HTTPClient}}.DecodeStream[{{.Stream.Frame}}](res, body, []{{$.HTTPClient}}.ResponseTarget{ {{- template "client/targets.tmpl" .EnvelopeTargets}} - }) + }, c.unmarshal) if err != nil { return out, err } {{- else}} if {{$.HTTPClient}}.IsStreaming(res) { out.{{.Stream.Field}} = {{$.HTTPClient}}.NewStream[{{.Stream.Frame}}](res) + out.{{.Stream.Field}}.Unmarshal = c.unmarshal } {{- end}} return out, nil @@ -125,7 +126,7 @@ func (c *{{$.Client}}) {{.Name}}Request(ctx {{$.Context}}.Context, opts *{{.Opti {{- range .Bodies}} case {{.IsSet}}: {{- if .Encoder}} - b.{{.Encoder}}({{.Value}}{{with .MediaType}}, {{.}}{{end}}{{with .Encoding}}, {{.}}{{end}}) + b.{{.Encoder}}({{.Value}}{{with .MediaType}}, {{.}}{{end}}{{with .Encoding}}, {{.}}{{end}}{{if .IsJSON}}, c.marshal{{end}}) {{- else}} return nil, {{$.Runtime}}.ContentTypeError({{.MediaType}}) {{- end}} diff --git a/internal/gen/client/testdata/client.core.golden b/internal/gen/client/testdata/client.core.golden index c3dea84e..1d17ada1 100644 --- a/internal/gen/client/testdata/client.core.golden +++ b/internal/gen/client/testdata/client.core.golden @@ -2,6 +2,7 @@ package api import ( "context" + "encoding/json" "net/http" "net/url" "time" @@ -87,13 +88,25 @@ func WithRequestEditor(fns ...RequestEditor) PetClientOption { } } +// WithJSON writes JSON bodies with marshal and reads them with unmarshal. A nil one panics. +func WithJSON(marshal func(v any) ([]byte, error), unmarshal func(data []byte, v any) error) PetClientOption { + if marshal == nil || unmarshal == nil { + panic("WithJSON: nil function") + } + return func(c *PetClient) { + c.marshal, c.unmarshal = marshal, unmarshal + } +} + // PetClient calls the API at a base URL. // A response outside 2xx, or a 2xx the spec does not list, is a *httpclient.APIError. type PetClient struct { - baseURL *url.URL - doer HTTPDoer - timeout time.Duration - editors []RequestEditor + baseURL *url.URL + doer HTTPDoer + timeout time.Duration + editors []RequestEditor + marshal func(v any) ([]byte, error) + unmarshal func(data []byte, v any) error } // NewPetClient returns a client of the API at baseURL. @@ -103,7 +116,13 @@ func NewPetClient(baseURL string, opts ...PetClientOption) (*PetClient, error) { return nil, err } - c := &PetClient{baseURL: u, doer: &http.Client{}, timeout: 5 * time.Second} + c := &PetClient{ + baseURL: u, + doer: &http.Client{}, + timeout: 5 * time.Second, + marshal: json.Marshal, + unmarshal: json.Unmarshal, + } for _, opt := range opts { opt(c) } diff --git a/internal/gen/client/testdata/client.operations.golden b/internal/gen/client/testdata/client.operations.golden index f5ab0849..71543702 100644 --- a/internal/gen/client/testdata/client.operations.golden +++ b/internal/gen/client/testdata/client.operations.golden @@ -27,7 +27,7 @@ func (c *PetClient) ListPets(ctx context.Context, opts *types.ListPetsRequestOpt if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(types.Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -51,7 +51,7 @@ func (c *PetClient) ListPetsWithResponse(ctx context.Context, opts *types.ListPe {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, {Status: "200", IsHeaders: true, Dst: &out.Headers200}, {Status: "default", IsHeaders: true, Dst: &out.HeadersDefault}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -97,7 +97,7 @@ func (c *PetClient) CreatePet(ctx context.Context, opts *types.CreatePetRequestO {Status: "4XX", MediaType: "application/problem+json", Dst: new(types.Problem)}, {Status: "5XX", MediaType: "application/problem+json", Dst: new(types.Problem)}, {Status: "409"}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -122,7 +122,7 @@ func (c *PetClient) CreatePetWithResponse(ctx context.Context, opts *types.Creat {Status: "4XX", MediaType: "application/problem+json", Dst: &out.ProblemJSON4XX}, {Status: "5XX", MediaType: "application/problem+json", Dst: &out.ProblemJSON5XX}, {Status: "201", IsHeaders: true, Dst: &out.Headers201}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -136,7 +136,7 @@ func (c *PetClient) CreatePetRequest(ctx context.Context, opts *types.CreatePetR b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.BodyJSON != nil: - b.JSONBody(opts.BodyJSON, "application/json") + b.JSONBody(opts.BodyJSON, "application/json", c.marshal) case opts.BodyForm != nil: b.FormBody(opts.BodyForm, nil) case opts.BodyMultipart != nil: @@ -171,7 +171,7 @@ func (c *PetClient) DeletePet(ctx context.Context, opts *types.DeletePetRequestO } return httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "404", MediaType: "application/json", Dst: new(types.Problem)}, - }) + }, c.unmarshal) } // DeletePetWithResponse calls DELETE /pets/{id} and returns the whole response. @@ -188,7 +188,7 @@ func (c *PetClient) DeletePetWithResponse(ctx context.Context, opts *types.Delet out := &types.DeletePetResponse{HTTPResponse: res, Body: body} if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "404", MediaType: "application/json", Dst: &out.JSON404}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -206,7 +206,7 @@ func (c *PetClient) DeletePetRequest(ctx context.Context, opts *types.DeletePetR b.QueryString(opts.Filter, runtime.Param{Name: "filter", IsRequired: false, IsJSON: false}) switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) } return c.newRequest(ctx, "DeletePet", b, editors) } @@ -225,7 +225,7 @@ func (c *PetClient) Ping(ctx context.Context, opts *types.PingRequestOptions, ed var out string if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return "", err } return out, nil @@ -248,7 +248,7 @@ func (c *PetClient) PingWithResponse(ctx context.Context, opts *types.PingReques {Status: "200", MediaType: "text/xml", Dst: &out.XML200}, {Status: "200", MediaType: "image/*", Dst: &out.Image200}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -263,7 +263,7 @@ func (c *PetClient) PingRequest(ctx context.Context, opts *types.PingRequestOpti b.QueryString(opts.BodyQueryString, runtime.Param{Name: "body", IsRequired: true, IsJSON: true}) switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) } return c.newRequest(ctx, "Ping", b, editors) } @@ -278,7 +278,7 @@ func (c *PetClient) Query(ctx context.Context, opts *types.QueryRequestOptions, if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // QueryWithResponse calls QUERY /pets/query and returns the whole response. @@ -324,7 +324,7 @@ func (c *PetClient) Chat(ctx context.Context, opts *types.ChatRequestOptions, ed if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(types.Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -346,7 +346,7 @@ func (c *PetClient) ChatWithResponse(ctx context.Context, opts *types.ChatReques {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "200", MediaType: "text/event-stream", Dst: &out.EventStream200}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -364,7 +364,7 @@ func (c *PetClient) ChatStream(ctx context.Context, opts *types.ChatRequestOptio } return httpclient.OpenStream[types.ChatResponseItem](res, body, []httpclient.ResponseTarget{ {Status: "default", MediaType: "application/json", Dst: new(types.Problem)}, - }) + }, c.unmarshal) } // ChatStreamWithResponse calls POST /chat and returns the whole response, with its stream. @@ -383,7 +383,7 @@ func (c *PetClient) ChatStreamWithResponse(ctx context.Context, opts *types.Chat {Status: "200", MediaType: "application/json", Dst: &out.JSON200}, {Status: "200", MediaType: "text/event-stream", Dst: &out.EventStream200}, {Status: "default", MediaType: "application/json", Dst: &out.JSONDefault}, - }) + }, c.unmarshal) if err != nil { return out, err } @@ -398,7 +398,7 @@ func (c *PetClient) ChatRequest(ctx context.Context, opts *types.ChatRequestOpti b := httpclient.NewRequestBuilder(http.MethodPost, "/chat") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -419,7 +419,7 @@ func (c *PetClient) Tail(ctx context.Context, opts *types.TailRequestOptions, ed var out *string if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -439,7 +439,7 @@ func (c *PetClient) TailWithResponse(ctx context.Context, opts *types.TailReques out := &types.TailResponse{HTTPResponse: res, Body: body} if err = httpclient.DecodeResponse(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out.Ndjson200}, - }); err != nil { + }, c.unmarshal); err != nil { return out, err } return out, nil @@ -455,7 +455,7 @@ func (c *PetClient) TailStream(ctx context.Context, opts *types.TailRequestOptio if err != nil { return nil, err } - return httpclient.OpenStream[[]byte](res, body, nil) + return httpclient.OpenStream[[]byte](res, body, nil, c.unmarshal) } // TailStreamWithResponse calls GET /tail and returns the whole response, with its stream. @@ -472,7 +472,7 @@ func (c *PetClient) TailStreamWithResponse(ctx context.Context, opts *types.Tail out := &types.TailResponse{HTTPResponse: res, Body: body} out.Stream200, err = httpclient.DecodeStream[[]byte](res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out.Ndjson200}, - }) + }, c.unmarshal) if err != nil { return out, err } diff --git a/internal/gen/client/testdata/client.operations.plain.golden b/internal/gen/client/testdata/client.operations.plain.golden index a6c1f101..727a9bb0 100644 --- a/internal/gen/client/testdata/client.operations.plain.golden +++ b/internal/gen/client/testdata/client.operations.plain.golden @@ -27,7 +27,7 @@ func (c *PetClient) ListPets(ctx context.Context, opts *types.ListPetsRequestOpt if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(types.Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -73,7 +73,7 @@ func (c *PetClient) CreatePet(ctx context.Context, opts *types.CreatePetRequestO {Status: "4XX", MediaType: "application/problem+json", Dst: new(types.Problem)}, {Status: "5XX", MediaType: "application/problem+json", Dst: new(types.Problem)}, {Status: "409"}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -87,7 +87,7 @@ func (c *PetClient) CreatePetRequest(ctx context.Context, opts *types.CreatePetR b := httpclient.NewRequestBuilder(http.MethodPost, "/pets") switch { case opts.BodyJSON != nil: - b.JSONBody(opts.BodyJSON, "application/json") + b.JSONBody(opts.BodyJSON, "application/json", c.marshal) case opts.BodyForm != nil: b.FormBody(opts.BodyForm, nil) case opts.BodyMultipart != nil: @@ -122,7 +122,7 @@ func (c *PetClient) DeletePet(ctx context.Context, opts *types.DeletePetRequestO } return httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "404", MediaType: "application/json", Dst: new(types.Problem)}, - }) + }, c.unmarshal) } // DeletePetRequest builds the request of DELETE /pets/{id}. @@ -137,7 +137,7 @@ func (c *PetClient) DeletePetRequest(ctx context.Context, opts *types.DeletePetR b.QueryString(opts.Filter, runtime.Param{Name: "filter", IsRequired: false, IsJSON: false}) switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) } return c.newRequest(ctx, "DeletePet", b, editors) } @@ -156,7 +156,7 @@ func (c *PetClient) Ping(ctx context.Context, opts *types.PingRequestOptions, ed var out string if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "text/plain", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return "", err } return out, nil @@ -171,7 +171,7 @@ func (c *PetClient) PingRequest(ctx context.Context, opts *types.PingRequestOpti b.QueryString(opts.BodyQueryString, runtime.Param{Name: "body", IsRequired: true, IsJSON: true}) switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) } return c.newRequest(ctx, "Ping", b, editors) } @@ -186,7 +186,7 @@ func (c *PetClient) Query(ctx context.Context, opts *types.QueryRequestOptions, if err != nil { return err } - return httpclient.DecodeSuccess(res, body, nil) + return httpclient.DecodeSuccess(res, body, nil, c.unmarshal) } // QueryRequest builds the request of QUERY /pets/query. @@ -217,7 +217,7 @@ func (c *PetClient) Chat(ctx context.Context, opts *types.ChatRequestOptions, ed if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &out}, {Status: "default", MediaType: "application/json", Dst: new(types.Problem)}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -235,7 +235,7 @@ func (c *PetClient) ChatStream(ctx context.Context, opts *types.ChatRequestOptio } return httpclient.OpenStream[types.ChatResponseItem](res, body, []httpclient.ResponseTarget{ {Status: "default", MediaType: "application/json", Dst: new(types.Problem)}, - }) + }, c.unmarshal) } // ChatRequest builds the request of POST /chat. @@ -246,7 +246,7 @@ func (c *PetClient) ChatRequest(ctx context.Context, opts *types.ChatRequestOpti b := httpclient.NewRequestBuilder(http.MethodPost, "/chat") switch { case opts.Body != nil: - b.JSONBody(opts.Body, "application/json") + b.JSONBody(opts.Body, "application/json", c.marshal) default: return nil, runtime.ErrBodyEmpty } @@ -267,7 +267,7 @@ func (c *PetClient) Tail(ctx context.Context, opts *types.TailRequestOptions, ed var out *string if err = httpclient.DecodeSuccess(res, body, []httpclient.ResponseTarget{ {Status: "200", MediaType: "application/x-ndjson", Dst: &out}, - }); err != nil { + }, c.unmarshal); err != nil { return nil, err } return out, nil @@ -283,7 +283,7 @@ func (c *PetClient) TailStream(ctx context.Context, opts *types.TailRequestOptio if err != nil { return nil, err } - return httpclient.OpenStream[[]byte](res, body, nil) + return httpclient.OpenStream[[]byte](res, body, nil, c.unmarshal) } // TailRequest builds the request of GET /tail. diff --git a/internal/gen/server/adapter.go b/internal/gen/server/adapter.go index bfacbc18..f2fbb3ca 100644 --- a/internal/gen/server/adapter.go +++ b/internal/gen/server/adapter.go @@ -65,7 +65,7 @@ type AdapterView struct { HTTPServer string Validation string HTTP string - IO string + JSON string Errors string MaxMemory int64 IsRequestValidated bool @@ -188,7 +188,7 @@ func adapterView(g *Generator, s *gocode.Scope) *AdapterView { Runtime: s.Import(gomodel.Import{Path: gomodel.RuntimePath}), HTTPServer: s.Import(gomodel.Import{Path: gomodel.HTTPServerPath}), HTTP: s.Import(gomodel.Import{Path: "net/http"}), - IO: s.Import(gomodel.Import{Path: "io"}), + JSON: s.Import(gomodel.Import{Path: "encoding/json"}), Errors: s.Import(gomodel.Import{Path: "errors"}), MaxMemory: g.opts.MultipartMaxMemory, IsRequestValidated: g.opts.ValidateRequest, diff --git a/internal/gen/server/templates/adapter.tmpl b/internal/gen/server/templates/adapter.tmpl index b456a4e8..a8fdd449 100644 --- a/internal/gen/server/templates/adapter.tmpl +++ b/internal/gen/server/templates/adapter.tmpl @@ -23,7 +23,8 @@ type ServerOptions struct { Router any Middleware []func({{.HTTP}}.Handler) {{.HTTP}}.Handler ErrorHandler {{.HTTPServer}}.ErrorHandler - JSONDecoder func(body {{.IO}}.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 {{- if .Presence}} Presence {{.Runtime}}.PresenceChecker @@ -37,7 +38,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: {{.HTTPServer}}.DefaultErrorHandler{}, - JSONDecoder: {{.Runtime}}.DecodeJSON, + JSONDecoder: {{.JSON}}.Unmarshal, + JSONEncoder: {{.JSON}}.Marshal, MultipartMaxMemory: {{.MaxMemory}}, {{- if .Presence}} Presence: bodyPresence, @@ -63,13 +65,20 @@ func WithErrorHandler(h {{.HTTPServer}}.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body {{.IO}}.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -220,7 +229,7 @@ func (a *HTTPAdapter) write(w {{.HTTP}}.ResponseWriter, r *{{.HTTP}}.Request, id if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := {{.HTTPServer}}.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := {{.HTTPServer}}.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case {{.Errors}}.Is(err, {{.Runtime}}.ErrContentType): a.fail(w, r, &{{.HTTPServer}}.HandlerError{Kind: {{.HTTPServer}}.ErrorResponse, OperationID: id, Err: err}) diff --git a/internal/gen/server/templates/body.tmpl b/internal/gen/server/templates/body.tmpl index 3dfbee25..564020a4 100644 --- a/internal/gen/server/templates/body.tmpl +++ b/internal/gen/server/templates/body.tmpl @@ -4,12 +4,12 @@ a.failBody(w, r, {{.OperationID}}, err) {{.Return}} } - if err = a.opts.JSONDecoder(body, {{.Target}}, {{.IsRequired}}); err != nil { + if err = {{.Runtime}}.DecodeJSON(body, {{.Target}}, {{.IsRequired}}, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, {{.OperationID}}, err) {{.Return}} } {{- else if .IsJSON}} - if err := a.opts.JSONDecoder(r.Body, {{.Target}}, {{.IsRequired}}); err != nil { + if err := {{.Runtime}}.DecodeJSON(r.Body, {{.Target}}, {{.IsRequired}}, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, {{.OperationID}}, err) {{.Return}} } diff --git a/internal/gen/server/testdata/server.adapter.echo-v5.golden b/internal/gen/server/testdata/server.adapter.echo-v5.golden index c6494932..1ec3e981 100644 --- a/internal/gen/server/testdata/server.adapter.echo-v5.golden +++ b/internal/gen/server/testdata/server.adapter.echo-v5.golden @@ -1,8 +1,8 @@ package api import ( + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v5" @@ -15,7 +15,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -26,7 +27,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 8388608, } for _, opt := range opts { @@ -49,13 +51,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -136,7 +145,7 @@ func (a *HTTPAdapter) CreatePet(c *echo.Context) error { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.BodyJSON, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return nil } @@ -219,7 +228,7 @@ func (a *HTTPAdapter) DeletePet(c *echo.Context) error { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "DeletePet", err) return nil } @@ -262,7 +271,7 @@ func (a *HTTPAdapter) Ping(c *echo.Context) error { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Ping", err) return nil } @@ -311,7 +320,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/internal/gen/server/testdata/server.adapter.echo.golden b/internal/gen/server/testdata/server.adapter.echo.golden index ac2e0cbc..34e34f12 100644 --- a/internal/gen/server/testdata/server.adapter.echo.golden +++ b/internal/gen/server/testdata/server.adapter.echo.golden @@ -1,8 +1,8 @@ package api import ( + "encoding/json" "errors" - "io" "net/http" echo "github.com/labstack/echo/v4" @@ -15,7 +15,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -26,7 +27,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 8388608, } for _, opt := range opts { @@ -49,13 +51,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -136,7 +145,7 @@ func (a *HTTPAdapter) CreatePet(c echo.Context) error { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.BodyJSON, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return nil } @@ -219,7 +228,7 @@ func (a *HTTPAdapter) DeletePet(c echo.Context) error { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "DeletePet", err) return nil } @@ -262,7 +271,7 @@ func (a *HTTPAdapter) Ping(c echo.Context) error { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Ping", err) return nil } @@ -311,7 +320,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/internal/gen/server/testdata/server.adapter.golden b/internal/gen/server/testdata/server.adapter.golden index 31e5564e..1f57a6ba 100644 --- a/internal/gen/server/testdata/server.adapter.golden +++ b/internal/gen/server/testdata/server.adapter.golden @@ -1,8 +1,8 @@ package api import ( + "encoding/json" "errors" - "io" "net/http" chi "github.com/go-chi/chi/v5" @@ -15,7 +15,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -26,7 +27,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 8388608, } for _, opt := range opts { @@ -49,13 +51,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -133,7 +142,7 @@ func (a *HTTPAdapter) CreatePet(w http.ResponseWriter, r *http.Request) { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.BodyJSON, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return } @@ -214,7 +223,7 @@ func (a *HTTPAdapter) DeletePet(w http.ResponseWriter, r *http.Request) { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "DeletePet", err) return } @@ -255,7 +264,7 @@ func (a *HTTPAdapter) Ping(w http.ResponseWriter, r *http.Request) { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Ping", err) return } @@ -303,7 +312,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/internal/gen/server/testdata/server.adapter.kratos.golden b/internal/gen/server/testdata/server.adapter.kratos.golden index 4afe3507..f035ed91 100644 --- a/internal/gen/server/testdata/server.adapter.kratos.golden +++ b/internal/gen/server/testdata/server.adapter.kratos.golden @@ -1,8 +1,8 @@ package api import ( + "encoding/json" "errors" - "io" "net/http" khttp "github.com/go-kratos/kratos/v2/transport/http" @@ -15,7 +15,8 @@ type ServerOptions struct { Router any Middleware []func(http.Handler) http.Handler ErrorHandler httpserver.ErrorHandler - JSONDecoder func(body io.Reader, dst any, isRequired bool) error + JSONDecoder func(data []byte, v any) error + JSONEncoder func(v any) ([]byte, error) MultipartMaxMemory int64 } @@ -26,7 +27,8 @@ type ServerOption func(*ServerOptions) func NewServerOptions(opts ...ServerOption) *ServerOptions { o := &ServerOptions{ ErrorHandler: httpserver.DefaultErrorHandler{}, - JSONDecoder: runtime.DecodeJSON, + JSONDecoder: json.Unmarshal, + JSONEncoder: json.Marshal, MultipartMaxMemory: 8388608, } for _, opt := range opts { @@ -49,13 +51,20 @@ func WithErrorHandler(h httpserver.ErrorHandler) ServerOption { } } -// WithJSONDecoder sets what reads JSON bodies. isRequired says whether an empty body is an error. -func WithJSONDecoder(decode func(body io.Reader, dst any, isRequired bool) error) ServerOption { +// WithJSONDecoder sets what reads JSON request bodies. +func WithJSONDecoder(decode func(data []byte, v any) error) ServerOption { return func(o *ServerOptions) { o.JSONDecoder = decode } } +// WithJSONEncoder sets what writes JSON response bodies and stream frames. +func WithJSONEncoder(encode func(v any) ([]byte, error)) ServerOption { + return func(o *ServerOptions) { + o.JSONEncoder = encode + } +} + // WithMultipartMaxMemory sets how much of a multipart form stays in memory. func WithMultipartMaxMemory(n int64) ServerOption { return func(o *ServerOptions) { @@ -136,7 +145,7 @@ func (a *HTTPAdapter) CreatePet(c khttp.Context) error { opts := &CreatePetServiceRequestOptions{RawRequest: r} switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.BodyJSON, true); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.BodyJSON, true, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "CreatePet", err) return nil } @@ -219,7 +228,7 @@ func (a *HTTPAdapter) DeletePet(c khttp.Context) error { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "DeletePet", err) return nil } @@ -262,7 +271,7 @@ func (a *HTTPAdapter) Ping(c khttp.Context) error { } switch contentType := runtime.ContentType(r.Header); contentType { case "application/json": - if err := a.opts.JSONDecoder(r.Body, &opts.Body, false); err != nil { + if err := runtime.DecodeJSON(r.Body, &opts.Body, false, a.opts.JSONDecoder); err != nil { a.failDecode(w, r, "Ping", err) return nil } @@ -311,7 +320,7 @@ func (a *HTTPAdapter) write(w http.ResponseWriter, r *http.Request, id string, r if res.ContentType() != "" { w.Header().Set("Content-Type", res.ContentType()) } - err := httpserver.Write(w, res.StatusCode(), res.Header(), res.Payload()) + err := httpserver.Writer{Marshal: a.opts.JSONEncoder}.Write(w, res.StatusCode(), res.Header(), res.Payload()) switch { case errors.Is(err, runtime.ErrContentType): a.fail(w, r, &httpserver.HandlerError{Kind: httpserver.ErrorResponse, OperationID: id, Err: err}) diff --git a/internal/gomodel/model.go b/internal/gomodel/model.go index 33f7db3d..a89e5f62 100644 --- a/internal/gomodel/model.go +++ b/internal/gomodel/model.go @@ -23,7 +23,7 @@ import ( // serverNames are what the server parts declare next to the models. var serverNames = []string{ "NewRouter", "HTTPAdapter", "NewHTTPAdapter", "ServerOption", "ServerOptions", "NewServerOptions", - "WithRouter", "WithMiddleware", "WithErrorHandler", "WithJSONDecoder", "WithMultipartMaxMemory", "WithPresence", + "WithRouter", "WithMiddleware", "WithErrorHandler", "WithJSONDecoder", "WithJSONEncoder", "WithMultipartMaxMemory", "WithPresence", "ErrorKind", "HandlerError", "ErrorHandler", "ErrorHandlerFunc", "DefaultErrorHandler", "ErrorParse", "ErrorDecode", "ErrorValidation", "ErrorService", "ErrorResponse", } @@ -176,7 +176,7 @@ func OptionsFrom(cfg *config.Config) Options { } if c := cfg.Client; c != nil { name := cmp.Or(c.Name, "Client") - opts.Reserved = append(opts.Reserved, name, "New"+name, n.ClientOption(name), n.Interface(name), "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor") + opts.Reserved = append(opts.Reserved, name, "New"+name, n.ClientOption(name), n.Interface(name), "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "WithJSON") opts.OperationSuffixes = append(opts.OperationSuffixes, n.ClientRequestOptions("")) opts.Methods.Client = []string{"Request"} stream := []string{"Stream"} diff --git a/internal/gomodel/model_test.go b/internal/gomodel/model_test.go index ecbeee12..5bf2a449 100644 --- a/internal/gomodel/model_test.go +++ b/internal/gomodel/model_test.go @@ -130,7 +130,7 @@ func TestOptionsFrom(t *testing.T) { HasResponseHeaders: true, Reserved: slices.Concat( []string{"PetServiceInterface"}, serverNames, []string{"PetService", "NewPetService", "ErrNotImplemented"}, middlewareNames, - []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor"}, + []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "WithJSON"}, ), Methods: Methods{Client: []string{"Request"}}, OperationSuffixes: []string{"ServiceRequestOptions", "ResponseData", "RequestOptions"}, @@ -149,7 +149,7 @@ func TestOptionsFrom(t *testing.T) { Namer: n, IsValidated: true, HasResponseHeaders: true, - Reserved: []string{"PetClient", "NewPetClient", "PetClientOption", "PetClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor"}, + Reserved: []string{"PetClient", "NewPetClient", "PetClientOption", "PetClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "WithJSON"}, Methods: Methods{Client: []string{"Request", "WithResponse"}}, OperationSuffixes: []string{"RequestOptions", "Response"}, }, @@ -166,7 +166,7 @@ func TestOptionsFrom(t *testing.T) { EnumPrefix: true, Namer: n, IsValidated: true, - Reserved: []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor"}, + Reserved: []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "WithJSON"}, Methods: Methods{Client: []string{"Request"}, Stream: []string{"Stream"}}, OperationSuffixes: []string{"RequestOptions"}, }, @@ -184,7 +184,7 @@ func TestOptionsFrom(t *testing.T) { Namer: n, IsValidated: true, HasResponseHeaders: true, - Reserved: []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor"}, + Reserved: []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "WithJSON"}, Methods: Methods{Client: []string{"Request", "WithResponse"}, Stream: []string{"Stream", "StreamWithResponse"}}, OperationSuffixes: []string{"RequestOptions", "Response"}, }, @@ -202,7 +202,7 @@ func TestOptionsFrom(t *testing.T) { EnumPrefix: true, Namer: n, IsValidated: true, - Reserved: []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "MCPTools", "NewMCPTools", "ErrMCPStreaming"}, + Reserved: []string{"Client", "NewClient", "ClientOption", "ClientInterface", "HTTPDoer", "RequestEditor", "WithHTTPClient", "WithTimeout", "WithRequestEditor", "WithJSON", "MCPTools", "NewMCPTools", "ErrMCPStreaming"}, ReservedOperations: []string{"Register"}, Methods: Methods{Client: []string{"Request"}, Tool: []string{"Tool"}, IsToolSkipped: true}, OperationSuffixes: []string{"RequestOptions", "ToolInput"}, diff --git a/pkg/codegen/generate_test.go b/pkg/codegen/generate_test.go index 5b3079bb..b97f596d 100644 --- a/pkg/codegen/generate_test.go +++ b/pkg/codegen/generate_test.go @@ -502,7 +502,7 @@ func TestGenerateImports(t *testing.T) { "imports: [{package: net/netip}, {package: example.com/shop/tenant, alias: tn}, {package: math/big}]\n" + "templates: {server.request-options-extra: \"Remote netip.Addr\\nTenant tn.ID\"}\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `"net/http"`, `"net/netip"`, `tn "example.com/shop/tenant"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `"net/http"`, `"net/netip"`, `tn "example.com/shop/tenant"`, ofModels, ofChi, ofRuntime, ofServer}, "models/models.go": nil, }, unused: []Diagnostic{{ @@ -516,7 +516,7 @@ func TestGenerateImports(t *testing.T) { cfg: base + "server: {framework: chi, scaffold: {service: ./api/service.go, main: ./cmd/main.go}}\n" + "imports: [{package: embed, alias: _}]\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `_ "embed"`, `"errors"`, `"io"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `_ "embed"`, `"encoding/json"`, `"errors"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, "api/service.go": {`"context"`, `_ "embed"`, `"errors"`}, "cmd/main.go": {`"context"`, `_ "embed"`, `"log/slog"`, `"net/http"`, `"os"`, `"os/signal"`, `"syscall"`, `"time"`, `"example.com/work/api"`}, "models/models.go": {`_ "embed"`}, @@ -527,7 +527,7 @@ func TestGenerateImports(t *testing.T) { cfg: base + "server: {framework: chi}\nimports: [{package: embed, alias: _}, {package: embed}]\n" + "templates: {server.request-options-extra: Files embed.FS}\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"embed"`, `"errors"`, `"io"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"embed"`, `"encoding/json"`, `"errors"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, "models/models.go": {`_ "embed"`}, }, }, @@ -535,7 +535,7 @@ func TestGenerateImports(t *testing.T) { name: "Path under _ and under a name no code names keeps its _ import", cfg: base + "server: {framework: chi}\nimports: [{package: example.com/shop/models, alias: _}, {package: example.com/shop/models}]\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `"net/http"`, `_ "example.com/shop/models"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `"net/http"`, `_ "example.com/shop/models"`, ofModels, ofChi, ofRuntime, ofServer}, "models/models.go": {`_ "example.com/shop/models"`}, }, unused: []Diagnostic{{ @@ -549,7 +549,7 @@ func TestGenerateImports(t *testing.T) { cfg: base + "server: {framework: chi}\nimports: [{package: example.com/shop/models}]\n" + "templates: {server.request-options-extra: Owner models.Owner}\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `"net/http"`, `"example.com/shop/models"`, `models2 "example.com/work/models"`, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `"net/http"`, `"example.com/shop/models"`, `models2 "example.com/work/models"`, ofChi, ofRuntime, ofServer}, "models/models.go": nil, }, }, @@ -557,7 +557,7 @@ func TestGenerateImports(t *testing.T) { name: "Listed package no code names changes no name", cfg: base + "server: {framework: chi}\nimports: [{package: example.com/shop/models}, {package: example.com/shop/context}]\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, "models/models.go": nil, }, unused: []Diagnostic{ @@ -569,7 +569,7 @@ func TestGenerateImports(t *testing.T) { name: "Listed alias of a package the generator imports is its name in the generated code too", cfg: base + "server: {framework: chi}\nimports: [{package: net/http, alias: web}]\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `web "net/http"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `web "net/http"`, ofModels, ofChi, ofRuntime, ofServer}, "models/models.go": nil, }, has: "\tRawRequest *web.Request\n", @@ -585,7 +585,7 @@ func TestGenerateImports(t *testing.T) { cfg: base + "server: {framework: chi}\nimports: [{package: example.com/work/api}, {package: example.com/work/models}]\n" + "templates: {server.request-options-extra: First models.Pet}\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, "models/models.go": nil, }, unused: []Diagnostic{{ @@ -601,7 +601,7 @@ func TestGenerateImports(t *testing.T) { "templates: {server.scaffold.service-fields: Tenant tn.ID}\n" + "extra-files: {./tenant/tenant.go: 'var Default tn.ID'}\n", want: map[string][]string{ - "api/gen.go": {`"context"`, `"errors"`, `"io"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, + "api/gen.go": {`"context"`, `"encoding/json"`, `"errors"`, `"net/http"`, ofModels, ofChi, ofRuntime, ofServer}, "api/service.go": {`"context"`, `"errors"`, `tn "example.com/shop/tenant"`}, "models/models.go": nil, "tenant/tenant.go": {`tn "example.com/shop/tenant"`}, diff --git a/pkg/runtime/body_decode.go b/pkg/runtime/body_decode.go index 6241b315..c689f13f 100644 --- a/pkg/runtime/body_decode.go +++ b/pkg/runtime/body_decode.go @@ -78,9 +78,9 @@ func ContentTypeError(mediaType string) error { return fmt.Errorf("%w: %s", ErrContentType, mediaType) } -// DecodeJSON decodes a JSON body into dst, a pointer. An empty body is an error when the body is -// required, and leaves dst as it is otherwise. -func DecodeJSON(body io.Reader, dst any, isRequired bool) error { +// DecodeJSON decodes a JSON body into dst, a pointer, with unmarshal. An empty body is an error +// when the body is required, and leaves dst as it is otherwise. +func DecodeJSON(body io.Reader, dst any, isRequired bool, unmarshal func(data []byte, v any) error) error { data, err := io.ReadAll(body) if err != nil { return err @@ -88,7 +88,7 @@ func DecodeJSON(body io.Reader, dst any, isRequired bool) error { if len(bytes.TrimSpace(data)) == 0 { return empty(isRequired) } - return json.Unmarshal(data, dst) + return unmarshal(data, dst) } // DecodeForm decodes an application/x-www-form-urlencoded body into dst, a pointer. Keys with diff --git a/pkg/runtime/body_decode_test.go b/pkg/runtime/body_decode_test.go index 254b89d1..4c9e97af 100644 --- a/pkg/runtime/body_decode_test.go +++ b/pkg/runtime/body_decode_test.go @@ -194,7 +194,7 @@ func TestDecodeJSON(t *testing.T) { t.Parallel() var got address - err := DecodeJSON(tc.body, &got, tc.isRequired) + err := DecodeJSON(tc.body, &got, tc.isRequired, json.Unmarshal) if tc.wantErr != nil { require.Error(t, err) diff --git a/pkg/runtime/httpclient/httpclient.go b/pkg/runtime/httpclient/httpclient.go index f7a9f290..ca8bf04e 100644 --- a/pkg/runtime/httpclient/httpclient.go +++ b/pkg/runtime/httpclient/httpclient.go @@ -140,12 +140,12 @@ func (b *RequestBuilder) CookieParam(v any, p runtime.Param) { b.cookies = append(b.cookies, cookies...) } -// JSONBody sends v as JSON under mediaType. -func (b *RequestBuilder) JSONBody(v any, mediaType string) { +// JSONBody sends v as JSON under mediaType, written by marshal. +func (b *RequestBuilder) JSONBody(v any, mediaType string, marshal func(v any) ([]byte, error)) { if b.err != nil { return } - data, err := json.Marshal(v) + data, err := marshal(v) b.setBody(data, mediaType, err) } diff --git a/pkg/runtime/httpclient/httpclient_test.go b/pkg/runtime/httpclient/httpclient_test.go index c64be37d..296c027c 100644 --- a/pkg/runtime/httpclient/httpclient_test.go +++ b/pkg/runtime/httpclient/httpclient_test.go @@ -8,6 +8,7 @@ package httpclient import ( "cmp" "context" + "encoding/json" "errors" "io" "mime/multipart" @@ -249,7 +250,7 @@ func TestRequestBuilder(t *testing.T) { b.QueryParam("x", runtime.Param{Name: "q"}) b.HeaderParam("x", runtime.Param{Name: "X-H"}) b.CookieParam("x", runtime.Param{Name: "c"}) - b.JSONBody(1, "application/json") + b.JSONBody(1, "application/json", json.Marshal) b.FormBody(color, nil) b.MultipartBody(color, nil) b.FileBody(runtime.NewFile(nil, "a", ""), "") @@ -325,14 +326,14 @@ func TestRequestBuilder(t *testing.T) { }, { name: "A JSON body", - build: func(b *RequestBuilder) { b.JSONBody(color, "application/vnd.color+json") }, + build: func(b *RequestBuilder) { b.JSONBody(color, "application/vnd.color+json", json.Marshal) }, wantHeader: http.Header{"Content-Type": {"application/vnd.color+json"}}, wantBody: `{"R":100,"G":200,"B":150}`, wantLength: 25, }, { name: "A JSON body that cannot be written", - build: func(b *RequestBuilder) { b.JSONBody(make(chan int), "application/json") }, + build: func(b *RequestBuilder) { b.JSONBody(make(chan int), "application/json", json.Marshal) }, wantErrText: "json: unsupported type: chan int", }, { diff --git a/pkg/runtime/httpclient/response.go b/pkg/runtime/httpclient/response.go index c21dc1fe..b4e9c0c5 100644 --- a/pkg/runtime/httpclient/response.go +++ b/pkg/runtime/httpclient/response.go @@ -9,7 +9,6 @@ package httpclient import ( "bytes" - "encoding/json" "fmt" "net/http" "reflect" @@ -74,11 +73,12 @@ func (e *APIError) Unwrap() error { // DecodeResponse fills the targets of a response: those of its status, else of its range, else of // default, taking the body target whose media type fits the response's best. A status nothing -// documents, a media type no target takes and an empty body leave everything as it is. -func DecodeResponse(res *http.Response, body []byte, targets []ResponseTarget) error { +// documents, a media type no target takes and an empty body leave everything as it is. JSON is +// read with unmarshal. +func DecodeResponse(res *http.Response, body []byte, targets []ResponseTarget, unmarshal func(data []byte, v any) error) error { m := match(res, targets) if m.body != nil && len(body) > 0 { - if err := decodeBody(body, res.Header, m.body); err != nil { + if err := decodeBody(body, res.Header, m.body, unmarshal); err != nil { return err } } @@ -94,12 +94,12 @@ func DecodeResponse(res *http.Response, body []byte, targets []ResponseTarget) e // no error. A body in a media type no target of the status takes is an error. A status outside 2xx // is an *APIError, which carries the body decoded into the target of the status when that is an // error type. -func DecodeSuccess(res *http.Response, body []byte, targets []ResponseTarget) error { +func DecodeSuccess(res *http.Response, body []byte, targets []ResponseTarget, unmarshal func(data []byte, v any) error) error { if res.StatusCode < 200 || res.StatusCode > 299 { apiErr := &APIError{StatusCode: res.StatusCode, Header: res.Header, Body: body} m := match(res, targets) if m.body != nil && len(body) > 0 { - if typed, ok := m.body.Dst.(error); ok && decodeBody(body, res.Header, m.body) == nil { + if typed, ok := m.body.Dst.(error); ok && decodeBody(body, res.Header, m.body, unmarshal) == nil { apiErr.Err = typed } } @@ -114,7 +114,7 @@ func DecodeSuccess(res *http.Response, body []byte, targets []ResponseTarget) er case len(body) == 0: return nil case m.body != nil: - return decodeBody(body, res.Header, m.body) + return decodeBody(body, res.Header, m.body, unmarshal) case m.isUntaken: return runtime.ContentTypeError(m.mediaType) } @@ -197,8 +197,9 @@ func mediaRank(documented, actual string) int { return 0 } -// decodeBody reads body into the Dst of t, a pointer, by the type of Dst and the media type in header. -func decodeBody(body []byte, header http.Header, t *ResponseTarget) error { +// decodeBody reads body into the Dst of t, a pointer, by the type of Dst and the media type in +// header, JSON with unmarshal. +func decodeBody(body []byte, header http.Header, t *ResponseTarget, unmarshal func(data []byte, v any) error) error { target := reflect.ValueOf(t.Dst) if target.Kind() != reflect.Pointer || target.IsNil() { return fmt.Errorf("%w: the target must be a pointer", runtime.ErrParamValue) @@ -230,7 +231,7 @@ func decodeBody(body []byte, header http.Header, t *ResponseTarget) error { case strings.HasPrefix(mediaType, "text/"): return runtime.DecodeTextValue(bytes.NewReader(body), t.Dst, false) } - return json.Unmarshal(body, t.Dst) + return unmarshal(body, t.Dst) } // untyped is body as a schema without a type holds it outside JSON: text under text/*, else bytes. diff --git a/pkg/runtime/httpclient/response_test.go b/pkg/runtime/httpclient/response_test.go index fde32373..a686dde8 100644 --- a/pkg/runtime/httpclient/response_test.go +++ b/pkg/runtime/httpclient/response_test.go @@ -7,6 +7,7 @@ package httpclient import ( "bytes" + "encoding/json" "mime/multipart" "net/http" "testing" @@ -106,7 +107,7 @@ func TestDecodeResponse(t *testing.T) { t.Parallel() var got envelope - err := DecodeResponse(tc.res, []byte(tc.body), envelopeTargets(&got)) + err := DecodeResponse(tc.res, []byte(tc.body), envelopeTargets(&got), json.Unmarshal) if tc.wantErr != "" { require.EqualError(t, err, tc.wantErr) @@ -124,9 +125,9 @@ func TestDecodeFormResponse(t *testing.T) { var got *rgb targets := []ResponseTarget{{Status: "200", MediaType: "application/x-www-form-urlencoded", Dst: &got}} - require.NoError(t, DecodeResponse(response(200, "application/x-www-form-urlencoded", nil), []byte("R=1&G=2"), targets)) + require.NoError(t, DecodeResponse(response(200, "application/x-www-form-urlencoded", nil), []byte("R=1&G=2"), targets, json.Unmarshal)) assert.Equal(t, &rgb{R: 1, G: 2}, got) - require.Error(t, DecodeResponse(response(200, "application/x-www-form-urlencoded", nil), []byte("R=%zz"), targets)) + require.Error(t, DecodeResponse(response(200, "application/x-www-form-urlencoded", nil), []byte("R=%zz"), targets, json.Unmarshal)) } func TestDecodeTextResponse(t *testing.T) { @@ -148,7 +149,7 @@ func TestDecodeTextResponse(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := DecodeSuccess(response(200, "text/plain", nil), []byte(tc.body), []ResponseTarget{{Status: "200", MediaType: tc.mediaType, Dst: tc.dst}}) + err := DecodeSuccess(response(200, "text/plain", nil), []byte(tc.body), []ResponseTarget{{Status: "200", MediaType: tc.mediaType, Dst: tc.dst}}, json.Unmarshal) require.NoError(t, err) assert.Equal(t, tc.want, tc.dst) @@ -168,15 +169,15 @@ func TestDecodeMultipartResponse(t *testing.T) { for _, mediaType := range []string{"multipart/form-data", "multipart/*"} { var got *rgb - require.NoError(t, DecodeSuccess(res, b.Bytes(), []ResponseTarget{{Status: "200", MediaType: mediaType, Dst: &got}})) + require.NoError(t, DecodeSuccess(res, b.Bytes(), []ResponseTarget{{Status: "200", MediaType: mediaType, Dst: &got}}, json.Unmarshal)) assert.Equal(t, &rgb{R: 1, G: 2}, got) } var held any - require.NoError(t, DecodeSuccess(res, b.Bytes(), []ResponseTarget{{Status: "200", MediaType: "multipart/*", Dst: &held}})) + require.NoError(t, DecodeSuccess(res, b.Bytes(), []ResponseTarget{{Status: "200", MediaType: "multipart/*", Dst: &held}}, json.Unmarshal)) assert.Equal(t, b.Bytes(), held) - err := DecodeSuccess(response(200, "multipart/form-data", nil), b.Bytes(), []ResponseTarget{{Status: "200", MediaType: "multipart/form-data", Dst: new(rgb)}}) + err := DecodeSuccess(response(200, "multipart/form-data", nil), b.Bytes(), []ResponseTarget{{Status: "200", MediaType: "multipart/form-data", Dst: new(rgb)}}, json.Unmarshal) require.ErrorIs(t, err, http.ErrMissingBoundary) } @@ -249,7 +250,7 @@ func TestDecodeSuccess(t *testing.T) { body = []byte(tc.body) } - err := DecodeSuccess(tc.res, body, list) + err := DecodeSuccess(tc.res, body, list, json.Unmarshal) switch { case tc.wantAPI != nil: @@ -320,7 +321,7 @@ func TestDecodeSuccessOfA2xx(t *testing.T) { t.Parallel() var got envelope - err := DecodeSuccess(response(tc.code, "application/json", nil), []byte(`{"R":1}`), tc.targets(&got)) + err := DecodeSuccess(response(tc.code, "application/json", nil), []byte(`{"R":1}`), tc.targets(&got), json.Unmarshal) assert.Equal(t, tc.want, got) if !tc.wantAPI { @@ -358,7 +359,7 @@ func TestDecodeUnderWildcard(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - err := DecodeSuccess(res, []byte(tc.body), []ResponseTarget{{Status: "200", MediaType: tc.mediaType, Dst: tc.dst}}) + err := DecodeSuccess(res, []byte(tc.body), []ResponseTarget{{Status: "200", MediaType: tc.mediaType, Dst: tc.dst}}, json.Unmarshal) require.NoError(t, err) assert.Equal(t, tc.want, tc.dst) @@ -384,7 +385,7 @@ func TestDecodeEdges(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - require.EqualError(t, DecodeResponse(res, []byte("{}"), []ResponseTarget{tc.target}), tc.wantErr) + require.EqualError(t, DecodeResponse(res, []byte("{}"), []ResponseTarget{tc.target}, json.Unmarshal), tc.wantErr) }) } } diff --git a/pkg/runtime/httpclient/stream.go b/pkg/runtime/httpclient/stream.go index 3a2d2f90..6732005e 100644 --- a/pkg/runtime/httpclient/stream.go +++ b/pkg/runtime/httpclient/stream.go @@ -46,12 +46,14 @@ type Event struct { // next frame into Current, and Err reports what stopped Next. The caller owns the response and // closes the stream. Sentinels are frames that end the stream instead of being decoded, such as // the [DONE] some APIs send last; MaxFrameSize is the most bytes one line of the body, or the -// data of one event, may hold, 0 for no limit. Set both before the first Next. Canceling the -// context of the request unblocks a pending Next, and Err then reports the context's error. -// Close, called from another goroutine, unblocks it too, and Err then reports nil. +// data of one event, may hold, 0 for no limit; Unmarshal reads a frame that is not []byte, +// json.Unmarshal when it is nil. Set them before the first Next. Canceling the context of the +// request unblocks a pending Next, and Err then reports the context's error. Close, called from +// another goroutine, unblocks it too, and Err then reports nil. type Stream[T any] struct { Sentinels []string MaxFrameSize int + Unmarshal func(data []byte, v any) error body io.ReadCloser frame func(limit int) (Event, bool, error) @@ -118,7 +120,7 @@ func (s *Stream[T]) Next() bool { if _, isRaw := any(¤t).(*[]byte); !isRaw && len(data) == 0 { continue } - if err = decodeFrame(event.Data, ¤t); err != nil { + if err = s.decode(event.Data, ¤t); err != nil { s.stop(fmt.Errorf("%w: %w", ErrFrame, err)) return false } @@ -170,6 +172,18 @@ func (s *Stream[T]) stop(err error) { s.isDone, s.err = true, err } +// decode reads data into dst: as it is into bytes, and with Unmarshal into anything else. +func (s *Stream[T]) decode(data []byte, dst any) error { + if raw, ok := dst.(*[]byte); ok { + *raw = bytes.Clone(data) + return nil + } + if s.Unmarshal == nil { + return json.Unmarshal(data, dst) + } + return s.Unmarshal(data, dst) +} + // streamBody is the body of a streamed response, whose request lives until the body is closed. type streamBody struct { io.ReadCloser @@ -311,23 +325,26 @@ func SendStream(d Doer, req *http.Request, mediaType string, timeout time.Durati // OpenStream returns a stream over the frames of a response SendStream returned with its body. // A 2xx response without a body, such as 204, is a stream without frames; one in another media // type is ErrContentType. A status outside 2xx is an *APIError, as DecodeSuccess reports it with -// targets. -func OpenStream[T any](res *http.Response, body []byte, targets []ResponseTarget) (*Stream[T], error) { +// targets. The stream and the error read JSON with unmarshal. +func OpenStream[T any](res *http.Response, body []byte, targets []ResponseTarget, unmarshal func(data []byte, v any) error) (*Stream[T], error) { isSuccess := res.StatusCode >= 200 && res.StatusCode <= 299 switch { case IsStreaming(res), isSuccess && len(body) == 0: - return NewStream[T](res), nil + s := NewStream[T](res) + s.Unmarshal = unmarshal + return s, nil case isSuccess: return nil, runtime.ContentTypeError(runtime.ContentType(res.Header)) } - return nil, DecodeSuccess(res, body, targets) + return nil, DecodeSuccess(res, body, targets, unmarshal) } // DecodeStream is DecodeResponse for a response SendStream returned with its body. A streamed // response fills the typed headers of its status and comes back as a stream, closed when a header -// does not decode. Any other response is decoded whole and gives no stream. -func DecodeStream[T any](res *http.Response, body []byte, targets []ResponseTarget) (*Stream[T], error) { - err := DecodeResponse(res, body, targets) +// does not decode. Any other response is decoded whole and gives no stream. JSON is read with +// unmarshal. +func DecodeStream[T any](res *http.Response, body []byte, targets []ResponseTarget, unmarshal func(data []byte, v any) error) (*Stream[T], error) { + err := DecodeResponse(res, body, targets, unmarshal) switch { case !IsStreaming(res): return nil, err @@ -335,7 +352,9 @@ func DecodeStream[T any](res *http.Response, body []byte, targets []ResponseTarg _ = res.Body.Close() return nil, err } - return NewStream[T](res), nil + s := NewStream[T](res) + s.Unmarshal = unmarshal + return s, nil } // bodyOf is the body of res, or an empty one when res has none. @@ -411,12 +430,3 @@ func parseRetry(value []byte) (time.Duration, bool) { } return time.Duration(ms) * time.Millisecond, true } - -// decodeFrame reads data into dst: as it is into bytes, and as JSON into anything else. -func decodeFrame(data []byte, dst any) error { - if raw, ok := dst.(*[]byte); ok { - *raw = bytes.Clone(data) - return nil - } - return json.Unmarshal(data, dst) -} diff --git a/pkg/runtime/httpclient/stream_test.go b/pkg/runtime/httpclient/stream_test.go index 47b43bf0..c7d2dc29 100644 --- a/pkg/runtime/httpclient/stream_test.go +++ b/pkg/runtime/httpclient/stream_test.go @@ -7,6 +7,7 @@ package httpclient import ( "context" + "encoding/json" "errors" "io" "net/http" @@ -444,7 +445,7 @@ func TestStreamStopUnblocksNext(t *testing.T) { require.NoError(t, err) res, body, err := SendStream(srv.Client(), req, mediaTypeEventStream, 0) require.NoError(t, err) - s, err := OpenStream[chunk](res, body, nil) + s, err := OpenStream[chunk](res, body, nil, json.Unmarshal) require.NoError(t, err) t.Cleanup(func() { _ = s.Close() }) @@ -620,7 +621,7 @@ func TestOpenStream(t *testing.T) { res, body, err := SendStream(d, req, "application/ndjson", 0) require.NoError(t, err) - s, err := OpenStream[chunk](res, body, targets) + s, err := OpenStream[chunk](res, body, targets, json.Unmarshal) if tc.wantErr != nil { require.Error(t, err) @@ -699,7 +700,7 @@ func TestDecodeStream(t *testing.T) { s, err := DecodeStream[chunk](res, data, []ResponseTarget{ {Status: "200", MediaType: "application/json", Dst: &json200}, {Status: "200", IsHeaders: true, Dst: &headers}, - }) + }, json.Unmarshal) if tc.wantErr != "" { require.EqualError(t, err, tc.wantErr) diff --git a/pkg/runtime/httpserver/write.go b/pkg/runtime/httpserver/write.go index a88818e3..9a1b4ec6 100644 --- a/pkg/runtime/httpserver/write.go +++ b/pkg/runtime/httpserver/write.go @@ -26,8 +26,14 @@ const mediaTypeEventStream = "text/event-stream" var lineBreaks = strings.NewReplacer("\r\n", "\n", "\r", "\n") +// Writer writes response bodies by their media type. Marshal writes JSON bodies and frames, +// json.Marshal when it is nil. +type Writer struct { + Marshal func(v any) ([]byte, error) +} + // Write writes the headers, then body encoded for the media type of its Content-Type. -func Write(w http.ResponseWriter, status int, headers http.Header, body any) error { +func (wr Writer) Write(w http.ResponseWriter, status int, headers http.Header, body any) error { for key, values := range headers { w.Header()[key] = values } @@ -44,7 +50,7 @@ func Write(w http.ResponseWriter, status int, headers http.Header, body any) err mediaType := runtime.ContentType(w.Header()) if runtime.IsJSON(mediaType) { - return writeJSON(w, status, body) + return wr.writeJSON(w, status, body) } if data, ok := rawBody(body); ok { return writeBytes(w, status, data) @@ -52,7 +58,7 @@ func Write(w http.ResponseWriter, status int, headers http.Header, body any) err v := reflect.ValueOf(body) switch { case mediaType == "": - return writeJSON(w, status, body) + return wr.writeJSON(w, status, body) case v.Kind() == reflect.Pointer && v.IsNil(): w.WriteHeader(status) return nil @@ -61,7 +67,7 @@ func Write(w http.ResponseWriter, status int, headers http.Header, body any) err case mediaType == "multipart/form-data": return writeMultipart(w, status, body) case runtime.IsSequential(mediaType): - return writeFrames(w, status, mediaType, body) + return wr.writeFrames(w, status, mediaType, body) case strings.HasPrefix(mediaType, "text/"): return writeText(w, status, body) } @@ -69,8 +75,8 @@ func Write(w http.ResponseWriter, status int, headers http.Header, body any) err } // writeJSON writes body as JSON, with the content type application/json unless one is set. -func writeJSON(w http.ResponseWriter, status int, body any) error { - data, err := json.Marshal(body) +func (wr Writer) writeJSON(w http.ResponseWriter, status int, body any) error { + data, err := wr.marshal(body) if err != nil { return err } @@ -80,6 +86,49 @@ func writeJSON(w http.ResponseWriter, status int, body any) error { return writeBytes(w, status, data) } +func (wr Writer) writeFrames(w http.ResponseWriter, status int, mediaType string, body any) error { + rc := http.NewResponseController(w) + w.WriteHeader(status) + if err := flush(rc); err != nil { + return err + } + + isEvents := mediaType == mediaTypeEventStream + for frame := range framesOf(body) { + data, ok := rawBody(frame) + if !ok { + var err error + if data, err = wr.marshal(frame); err != nil { + return cut(err) + } + } + + var buf bytes.Buffer + if isEvents { + for line := range strings.SplitSeq(lineBreaks.Replace(string(data)), "\n") { + buf.WriteString("data: " + line + "\n") + } + } else { + buf.Write(data) + } + buf.WriteByte('\n') + if _, err := w.Write(buf.Bytes()); err != nil { + return cut(err) + } + if err := flush(rc); err != nil { + return err + } + } + return nil +} + +func (wr Writer) marshal(v any) ([]byte, error) { + if wr.Marshal == nil { + return json.Marshal(v) + } + return wr.Marshal(v) +} + // writeBytes writes data as it is, as application/octet-stream unless a content type is set. func writeBytes(w http.ResponseWriter, status int, data []byte) error { if w.Header().Get("Content-Type") == "" { @@ -147,42 +196,6 @@ func writeMultipart(w http.ResponseWriter, status int, body any) error { return cut(runtime.WriteMultipart(mw, body, nil)) } -func writeFrames(w http.ResponseWriter, status int, mediaType string, body any) error { - rc := http.NewResponseController(w) - w.WriteHeader(status) - if err := flush(rc); err != nil { - return err - } - - isEvents := mediaType == mediaTypeEventStream - for frame := range framesOf(body) { - data, ok := rawBody(frame) - if !ok { - var err error - if data, err = json.Marshal(frame); err != nil { - return cut(err) - } - } - - var buf bytes.Buffer - if isEvents { - for line := range strings.SplitSeq(lineBreaks.Replace(string(data)), "\n") { - buf.WriteString("data: " + line + "\n") - } - } else { - buf.Write(data) - } - buf.WriteByte('\n') - if _, err := w.Write(buf.Bytes()); err != nil { - return cut(err) - } - if err := flush(rc); err != nil { - return err - } - } - return nil -} - func framesOf(body any) iter.Seq[any] { v := reflect.ValueOf(body) if v.Kind() != reflect.Func || !v.Type().CanSeq() { diff --git a/pkg/runtime/httpserver/write_test.go b/pkg/runtime/httpserver/write_test.go index a4785f9d..25418553 100644 --- a/pkg/runtime/httpserver/write_test.go +++ b/pkg/runtime/httpserver/write_test.go @@ -6,6 +6,8 @@ package httpserver import ( + "bytes" + "encoding/json" "errors" "io" "iter" @@ -88,7 +90,7 @@ func TestWrite(t *testing.T) { t.Parallel() w := httptest.NewRecorder() - require.NoError(t, Write(w, http.StatusCreated, tc.headers, tc.body)) + require.NoError(t, Writer{}.Write(w, http.StatusCreated, tc.headers, tc.body)) assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, tc.wantContentType, w.Header().Get("Content-Type")) @@ -97,19 +99,48 @@ func TestWrite(t *testing.T) { } } +func TestWriterMarshal(t *testing.T) { + t.Parallel() + + upper := func(v any) ([]byte, error) { + data, err := json.Marshal(v) + return bytes.ToUpper(data), err + } + tests := []struct { + name string + headers http.Header + body any + wantBody string + }{ + {name: "A JSON body", body: map[string]string{"a": "b"}, wantBody: `{"A":"B"}`}, + {name: "Frames", headers: http.Header{"Content-Type": {"application/x-ndjson"}}, body: slices.Values([]map[string]string{{"a": "b"}}), wantBody: "{\"A\":\"B\"}\n"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + require.NoError(t, Writer{Marshal: upper}.Write(w, http.StatusOK, tc.headers, tc.body)) + + assert.Equal(t, tc.wantBody, w.Body.String()) + }) + } +} + func TestWriteErrors(t *testing.T) { t.Parallel() xml := http.Header{"Content-Type": {"application/xml"}} - require.Error(t, Write(httptest.NewRecorder(), 200, nil, func() {})) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, nil, runtime.NewFileReader(iotest.ErrReader(io.ErrUnexpectedEOF), "", "", -1)), ErrResponseCut) + require.Error(t, Writer{}.Write(httptest.NewRecorder(), 200, nil, func() {})) + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, nil, runtime.NewFileReader(iotest.ErrReader(io.ErrUnexpectedEOF), "", "", -1)), ErrResponseCut) require.Error(t, writeFile(httptest.NewRecorder(), 200, runtime.NewFileFromMultipart(&multipart.FileHeader{Filename: "gone"}))) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, xml, struct{}{}), runtime.ErrContentType) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"application/x-www-form-urlencoded"}}, 1), runtime.ErrBodyValue) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"multipart/form-data"}}, 1), runtime.ErrBodyValue) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"text/plain"}}, make(chan int)), runtime.ErrParamValue) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"text/event-stream"}}, slices.Values([]func(){nil})), ErrResponseCut) - require.ErrorIs(t, Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"multipart/form-data"}}, struct { + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, xml, struct{}{}), runtime.ErrContentType) + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"application/x-www-form-urlencoded"}}, 1), runtime.ErrBodyValue) + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"multipart/form-data"}}, 1), runtime.ErrBodyValue) + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"text/plain"}}, make(chan int)), runtime.ErrParamValue) + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"text/event-stream"}}, slices.Values([]func(){nil})), ErrResponseCut) + require.ErrorIs(t, Writer{}.Write(httptest.NewRecorder(), 200, http.Header{"Content-Type": {"multipart/form-data"}}, struct { File runtime.File `json:"file"` }{File: runtime.NewFileReader(iotest.ErrReader(io.ErrUnexpectedEOF), "a", "", -1)}), ErrResponseCut) } @@ -118,7 +149,7 @@ func TestWriteMultipartResponse(t *testing.T) { t.Parallel() w := httptest.NewRecorder() - require.NoError(t, Write(w, http.StatusOK, http.Header{"Content-Type": {"multipart/form-data"}}, struct { + require.NoError(t, Writer{}.Write(w, http.StatusOK, http.Header{"Content-Type": {"multipart/form-data"}}, struct { Title string `json:"title"` }{Title: "Cat"})) @@ -151,7 +182,7 @@ func TestWriteCut(t *testing.T) { t.Parallel() tc.w.ResponseRecorder = httptest.NewRecorder() - require.ErrorIs(t, Write(tc.w, http.StatusOK, tc.headers, tc.body), ErrResponseCut) + require.ErrorIs(t, Writer{}.Write(tc.w, http.StatusOK, tc.headers, tc.body), ErrResponseCut) }) } } @@ -160,7 +191,7 @@ func TestWriteFlushesEachFrame(t *testing.T) { t.Parallel() w := &brokenWriter{ResponseRecorder: httptest.NewRecorder()} - require.NoError(t, Write(w, http.StatusOK, http.Header{"Content-Type": {"application/jsonl"}}, slices.Values([]int{0, 1}))) + require.NoError(t, Writer{}.Write(w, http.StatusOK, http.Header{"Content-Type": {"application/jsonl"}}, slices.Values([]int{0, 1}))) assert.Equal(t, 3, w.flushes) assert.Equal(t, "0\n1\n", w.Body.String())