diff --git a/README.md b/README.md index 707ae524..61a591b5 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Features include: - JSON Errors using [RFC9457](https://datatracker.ietf.org/doc/html/rfc9457) and `application/problem+json` by default (but can be changed) - Per-operation request size limits with sane defaults - [Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) between server and client - - Support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)) and CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header with the default config. + - Support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)) and optionally CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header with the default config. - Conditional requests support, e.g. `If-Match` or `If-Unmodified-Since` header utilities. - Optional automatic generation of `PATCH` operations that support: - [RFC 7386](https://www.rfc-editor.org/rfc/rfc7386) JSON Merge Patch @@ -104,6 +104,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. Pass `--port` or set the `SERVICE_PORT` env var. diff --git a/defaults.go b/defaults.go index a33ef5bb..362d292f 100644 --- a/defaults.go +++ b/defaults.go @@ -3,8 +3,6 @@ package huma import ( "encoding/json" "io" - - "github.com/fxamacker/cbor/v2" ) // DefaultJSONFormat is the default JSON formatter that can be set in the API's @@ -22,39 +20,24 @@ var DefaultJSONFormat = Format{ Unmarshal: json.Unmarshal, } -var cborEncMode, _ = cbor.EncOptions{ - // Canonical enc opts - Sort: cbor.SortCanonical, - ShortestFloat: cbor.ShortestFloat16, - NaNConvert: cbor.NaNConvert7e00, - InfConvert: cbor.InfConvertFloat16, - IndefLength: cbor.IndefLengthForbidden, - // Time handling - Time: cbor.TimeUnixDynamic, - TimeTag: cbor.EncTagRequired, -}.EncMode() - -// DefaultCBORFormat is the default CBOR formatter that can be set in the API's -// `Config.Formats` map. This is used by the `DefaultConfig` function. +// DefaultFormats is a map of default formats that can be set in the API's +// `Config.Formats` map, used for content negotiation for marshaling and +// unmarshaling request/response bodies. This is used by the `DefaultConfig` +// function and can be modified to add or remove additional formats. For +// example, to add support for CBOR, simply import it: // -// config := huma.Config{} -// config.Formats = map[string]huma.Format{ -// "application/cbor": huma.DefaultCBORFormat, -// "cbor": huma.DefaultCBORFormat, -// } -var DefaultCBORFormat = Format{ - Marshal: func(w io.Writer, v any) error { - return cborEncMode.NewEncoder(w).Encode(v) - }, - Unmarshal: cbor.Unmarshal, +// import _ "github.com/danielgtaylor/huma/v2/formats/cbor" +var DefaultFormats = map[string]Format{ + "application/json": DefaultJSONFormat, + "json": DefaultJSONFormat, } // DefaultConfig returns a default configuration for a new API. It is a good -// starting point for creating your own configuration. It supports JSON and -// CBOR formats out of the box. The registry uses references for structs and -// a link transformer is included to add `$schema` fields and links into -// responses. The `/openapi.[json|yaml]`, `/docs`, and `/schemas` paths are -// set up to serve the OpenAPI spec, docs UI, and schemas respectively. +// starting point for creating your own configuration. It supports the JSON +// format out of the box. The registry uses references for structs and a link +// transformer is included to add `$schema` fields and links into responses. The +// `/openapi.[json|yaml]`, `/docs`, and `/schemas` paths are set up to serve the +// OpenAPI spec, docs UI, and schemas respectively. // // // Create and customize the config (if desired). // config := huma.DefaultConfig("My API", "1.0.0") @@ -62,6 +45,11 @@ var DefaultCBORFormat = Format{ // // Create the API using the config. // router := chi.NewMux() // api := humachi.New(router, config) +// +// If desired, CBOR (a binary format similar to JSON) support can be +// automatically enabled by importing the CBOR package: +// +// import _ "github.com/danielgtaylor/huma/v2/formats/cbor" func DefaultConfig(title, version string) Config { schemaPrefix := "#/components/schemas/" schemasPath := "/schemas" @@ -79,15 +67,10 @@ func DefaultConfig(title, version string) Config { Schemas: registry, }, }, - OpenAPIPath: "/openapi", - DocsPath: "/docs", - SchemasPath: schemasPath, - Formats: map[string]Format{ - "application/json": DefaultJSONFormat, - "json": DefaultJSONFormat, - "application/cbor": DefaultCBORFormat, - "cbor": DefaultCBORFormat, - }, + OpenAPIPath: "/openapi", + DocsPath: "/docs", + SchemasPath: schemasPath, + Formats: DefaultFormats, DefaultFormat: "application/json", CreateHooks: []func(Config) Config{ func(c Config) Config { diff --git a/docs/docs/features/index.md b/docs/docs/features/index.md index ad1cbaef..21652e5e 100644 --- a/docs/docs/features/index.md +++ b/docs/docs/features/index.md @@ -26,7 +26,7 @@ Features include: - JSON Errors using [RFC9457](https://tools.ietf.org/html/rfc9457) and `application/problem+json` by default (but can be changed) - Per-operation request size limits with sane defaults - [Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) between server and client - - Support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)) and CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header with the default config. + - Support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)) and optional CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header with the default config. - Conditional requests support, e.g. `If-Match` or `If-Unmodified-Since` header utilities. - Optional automatic generation of `PATCH` operations that support: - [RFC 7386](https://www.rfc-editor.org/rfc/rfc7386) JSON Merge Patch diff --git a/docs/docs/features/response-serialization.md b/docs/docs/features/response-serialization.md index 98612d77..9409bd65 100644 --- a/docs/docs/features/response-serialization.md +++ b/docs/docs/features/response-serialization.md @@ -10,10 +10,23 @@ When handler functions return Go objects, they will be serialized to bytes for t The [`config.Formats`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Config) maps either a content type name or extension (suffix) to a `huma.Format` instance. -The default configuration for Huma includes support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)) and CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header. This is done by registering the following content types using [`huma.DefaultJSONFormat`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#DefaultJSONFormat) & [`huma.DefaultCBORFormat`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#DefaultCBORFormat): +The default configuration for Huma includes support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)) and optionally CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header. This is done by registering the following content types using [`huma.DefaultJSONFormat`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#DefaultJSONFormat): - `application/json` - Anything ending with `+json` + +CBOR support can be enabled by importing the `cbor` package, which adds [`cbor.DefaultCBORFormat`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2/formats/cbor#DefaultCBORFormat) to the default list of formats: + +```go title="main.go" +import ( + "github.com/danielgtaylor/huma/v2" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" +) +``` + +This adds the following content types: + - `application/cbor` - Anything ending with `+cbor` diff --git a/docs/docs/how-to/graceful-shutdown.md b/docs/docs/how-to/graceful-shutdown.md index f002eecf..8ac91e54 100644 --- a/docs/docs/how-to/graceful-shutdown.md +++ b/docs/docs/how-to/graceful-shutdown.md @@ -12,7 +12,7 @@ This can be accomplished in Huma using the CLI `hooks.OnStop()` hook, passing a ## Example -```go title="code.go" linenums="1" hl_lines="6-7 50-67" +```go title="code.go" linenums="1" hl_lines="6-7 51-68" package main import ( diff --git a/docs/docs/how-to/image-response.md b/docs/docs/how-to/image-response.md index f6761ad9..accb1a20 100644 --- a/docs/docs/how-to/image-response.md +++ b/docs/docs/how-to/image-response.md @@ -10,7 +10,7 @@ Images or other encoded or binary responses can be returned by simply using a `[ ## Example -```go title="code.go" linenums="1" hl_lines="18-22 31-50" +```go title="code.go" linenums="1" hl_lines="19-23 32-51" package main import ( diff --git a/docs/docs/terminal/install.cast b/docs/docs/terminal/install.cast index beb0a893..af75815f 100644 --- a/docs/docs/terminal/install.cast +++ b/docs/docs/terminal/install.cast @@ -191,5 +191,4 @@ [9.540756, "o", "v"] [9.580533, "o", "2"] [9.621607, "o", "\r\n\u001b[0m"] -[9.860282, "o", "go: added github.com/danielgtaylor/casing v0.0.0-20210126043903-4e55e6373ac3\r\n\u001b[0mgo: added github.com/danielgtaylor/huma/v2 v2.0.0-beta.3\r\n\u001b[0mgo: added github.com/danielgtaylor/mexpr v1.8.0\r\n...\r\n\u001b[0mgo: added gopkg.in/ini.v1 v1.67.0\r\n\u001b[0mgo: added gopkg.in/yaml.v3 v3.0.1\r\n\u001b[0m"] - +[9.860282, "o", "...\r\n\u001b[0m"] diff --git a/docs/docs/tutorial/client-sdks.md b/docs/docs/tutorial/client-sdks.md index 2a8120a0..1a488dc6 100644 --- a/docs/docs/tutorial/client-sdks.md +++ b/docs/docs/tutorial/client-sdks.md @@ -10,7 +10,7 @@ description: Level up your API with a generated Go SDK and client that uses it. First, let's create a command to grab the OpenAPI spec so the service doesn't need to be running and you can generate the SDK as needed (e.g. as part of the API service release process). -```go title="main.go" linenums="1" hl_lines="67 73 84-94" +```go title="main.go" linenums="1" hl_lines="69 75 86-96" package main import ( @@ -22,6 +22,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/docs/docs/tutorial/installation.md b/docs/docs/tutorial/installation.md index a2f27212..dd89f0d5 100644 --- a/docs/docs/tutorial/installation.md +++ b/docs/docs/tutorial/installation.md @@ -12,7 +12,7 @@ Huma requires [Go 1.20 or newer](https://go.dev/dl/), so install that first. You Next, open a terminal and create a new Go project, then go get the Huma dependency to it's ready to be imported: -{{ asciinema("../../terminal/install.cast", rows="18") }} +{{ asciinema("../../terminal/install.cast", rows="12") }} You should now have a directory structure like this: diff --git a/docs/docs/tutorial/sending-data.md b/docs/docs/tutorial/sending-data.md index 956610c4..9d348783 100644 --- a/docs/docs/tutorial/sending-data.md +++ b/docs/docs/tutorial/sending-data.md @@ -22,7 +22,7 @@ Response: 201 Created Add a new operation to our API that allows users to submit reviews of our product. -```go title="main.go" linenums="1" hl_lines="25-32 57-68" +```go title="main.go" linenums="1" hl_lines="28-35 60-71" package main import ( @@ -34,6 +34,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/docs/docs/tutorial/service-configuration.md b/docs/docs/tutorial/service-configuration.md index a3ce612b..802c2d9c 100644 --- a/docs/docs/tutorial/service-configuration.md +++ b/docs/docs/tutorial/service-configuration.md @@ -10,7 +10,7 @@ Huma includes a basic command-line and environment variable option parser that c [Your first API](your-first-api.md#operation) can be updated to take an optional network port parameter like this: -```go title="main.go" linenums="1" hl_lines="13-16 26-27 48-56" +```go title="main.go" linenums="1" hl_lines="16-19 29-30 51-59" package main import ( @@ -22,6 +22,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/docs/docs/tutorial/writing-tests.md b/docs/docs/tutorial/writing-tests.md index 7ca903e1..eba70d65 100644 --- a/docs/docs/tutorial/writing-tests.md +++ b/docs/docs/tutorial/writing-tests.md @@ -10,7 +10,7 @@ Huma provides a number of helpers for testing your API. The most important is th First, modify the service code to make it easier to test, by moving the operation registration code out of the `main` function: -```go title="main.go" linenums="1" hl_lines="34 63 72" +```go title="main.go" linenums="1" hl_lines="37 66 75" package main import ( @@ -22,6 +22,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/docs/docs/tutorial/your-first-api.md b/docs/docs/tutorial/your-first-api.md index 5dade938..6dbcd328 100644 --- a/docs/docs/tutorial/your-first-api.md +++ b/docs/docs/tutorial/your-first-api.md @@ -48,7 +48,7 @@ my-api/ Let's create a router, which will handle getting incoming requests to the correct operation handler, and a new API instance where we can register our operation. -```go title="main.go" linenums="1" hl_lines="3-9 18-27" +```go title="main.go" linenums="1" hl_lines="3-11 20-29" package main import ( @@ -57,6 +57,8 @@ import ( "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // GreetingOutput represents the greeting operation response. @@ -93,7 +95,7 @@ func main() { Register the operation with the Huma API instance, including how it maps to a URL. The handler function will take in a struct that defines its inputs (in this case a path parameter named `name`) and return the `GreetingOutput` model we built above. -```go title="main.go" linenums="1" hl_lines="4-5 25-32" +```go title="main.go" linenums="1" hl_lines="4-5 27-34" package main import ( @@ -104,6 +106,8 @@ import ( "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // GreetingOutput represents the greeting operation response. @@ -180,7 +184,7 @@ These docs are generated from the OpenAPI specification. You can use this file t You can use `huma.Register` to add more information to the OpenAPI specification, such as descriptions with Markdown, examples, tags, and more. The `huma.Operation` struct provides full access to the OpenAPI including the ability to add extensions. See the [`huma.Operation`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Operation) struct for more details. -```go title="main.go" linenums="1" hl_lines="26-33" +```go title="main.go" linenums="1" hl_lines="28-35" package main import ( @@ -191,6 +195,8 @@ import ( "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // GreetingOutput represents the greeting operation response. diff --git a/examples/cookies/main.go b/examples/cookies/main.go index df89bc7d..f56b90d4 100644 --- a/examples/cookies/main.go +++ b/examples/cookies/main.go @@ -10,6 +10,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/custom-error/main.go b/examples/custom-error/main.go index 82e2c640..f58f9cd3 100644 --- a/examples/custom-error/main.go +++ b/examples/custom-error/main.go @@ -8,6 +8,8 @@ import ( "github.com/danielgtaylor/huma/v2" "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) type MyError struct { diff --git a/examples/greet/main.go b/examples/greet/main.go index c3cea975..46bbff31 100644 --- a/examples/greet/main.go +++ b/examples/greet/main.go @@ -9,6 +9,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/omit/main.go b/examples/omit/main.go index f2121817..bf37a5b4 100644 --- a/examples/omit/main.go +++ b/examples/omit/main.go @@ -26,6 +26,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/oneof-response/main.go b/examples/oneof-response/main.go index d43fdc38..29cd25e4 100644 --- a/examples/oneof-response/main.go +++ b/examples/oneof-response/main.go @@ -18,6 +18,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/param-reuse/main.go b/examples/param-reuse/main.go index 211749cf..1609b826 100644 --- a/examples/param-reuse/main.go +++ b/examples/param-reuse/main.go @@ -13,6 +13,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/resolver/main.go b/examples/resolver/main.go index dd16a210..33c69e65 100644 --- a/examples/resolver/main.go +++ b/examples/resolver/main.go @@ -17,6 +17,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/spec-cmd/main.go b/examples/spec-cmd/main.go index 95412dc1..68a02f07 100644 --- a/examples/spec-cmd/main.go +++ b/examples/spec-cmd/main.go @@ -18,6 +18,8 @@ import ( "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi/v5" "github.com/spf13/cobra" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/sse/main.go b/examples/sse/main.go index cdf28aae..55de65ab 100644 --- a/examples/sse/main.go +++ b/examples/sse/main.go @@ -25,6 +25,8 @@ import ( "github.com/danielgtaylor/huma/v2/humacli" "github.com/danielgtaylor/huma/v2/sse" "github.com/go-chi/chi/v5" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/examples/v1-middleware/main.go b/examples/v1-middleware/main.go index cd2da44e..91bca084 100644 --- a/examples/v1-middleware/main.go +++ b/examples/v1-middleware/main.go @@ -12,6 +12,8 @@ import ( "github.com/danielgtaylor/huma/v2/adapters/humachi" "github.com/danielgtaylor/huma/v2/humacli" "github.com/go-chi/chi" + + _ "github.com/danielgtaylor/huma/v2/formats/cbor" ) // Options for the CLI. diff --git a/formats/cbor/cbor.go b/formats/cbor/cbor.go new file mode 100644 index 00000000..a4c6aced --- /dev/null +++ b/formats/cbor/cbor.go @@ -0,0 +1,43 @@ +// Package cbor provides a CBOR formatter for Huma with default configuration. +// Importing this package adds CBOR support to `huma.DefaultFormats`. +package cbor + +import ( + "io" + + "github.com/danielgtaylor/huma/v2" + "github.com/fxamacker/cbor/v2" +) + +var cborEncMode, _ = cbor.EncOptions{ + // Canonical enc opts + Sort: cbor.SortCanonical, + ShortestFloat: cbor.ShortestFloat16, + NaNConvert: cbor.NaNConvert7e00, + InfConvert: cbor.InfConvertFloat16, + IndefLength: cbor.IndefLengthForbidden, + // Time handling + Time: cbor.TimeUnixDynamic, + TimeTag: cbor.EncTagRequired, +}.EncMode() + +// DefaultCBORFormat is the default CBOR formatter that can be set in the API's +// `Config.Formats` map. This is usually not needed as importing this package +// automatically adds the CBOR format to the default formats. +// +// config := huma.Config{} +// config.Formats = map[string]huma.Format{ +// "application/cbor": huma.DefaultCBORFormat, +// "cbor": huma.DefaultCBORFormat, +// } +var DefaultCBORFormat = huma.Format{ + Marshal: func(w io.Writer, v any) error { + return cborEncMode.NewEncoder(w).Encode(v) + }, + Unmarshal: cbor.Unmarshal, +} + +func init() { + huma.DefaultFormats["application/cbor"] = DefaultCBORFormat + huma.DefaultFormats["cbor"] = DefaultCBORFormat +} diff --git a/formats/cbor/cbor_test.go b/formats/cbor/cbor_test.go new file mode 100644 index 00000000..c53716a1 --- /dev/null +++ b/formats/cbor/cbor_test.go @@ -0,0 +1,20 @@ +package cbor + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRoundTrip(t *testing.T) { + data := map[any]any{"hello": "world"} + + buf := &bytes.Buffer{} + require.NoError(t, DefaultCBORFormat.Marshal(buf, data)) + + var v any + require.NoError(t, DefaultCBORFormat.Unmarshal(buf.Bytes(), &v)) + + require.Equal(t, data, v) +}