From 0a90edd3f4e7e0620abe414c4069a8930e282bd9 Mon Sep 17 00:00:00 2001 From: "Daniel G. Taylor" Date: Thu, 26 Oct 2023 15:59:21 -0700 Subject: [PATCH] feat: split transform/marshal, reorg tests & dogfood humatest --- api.go | 25 ++- api_test.go | 59 +++++- cli_test.go | 74 ++++++- error.go | 6 +- error_test.go | 60 +++--- huma.go | 34 +++- huma_test.go | 521 ++++++++++++++++------------------------------- schema_test.go | 39 ++-- validate_test.go | 123 +++++------ 9 files changed, 459 insertions(+), 482 deletions(-) diff --git a/api.go b/api.go index 304aa828..5293113a 100644 --- a/api.go +++ b/api.go @@ -116,10 +116,15 @@ type API interface { // send an `accept` header, then JSON is used. Negotiate(accept string) (string, error) - // Marshal marshals the given value into the given writer. The content type - // is used to determine which format to use. Use `Negotiate` to get the - // content type from an accept header. TODO: update - Marshal(ctx Context, respKey string, contentType string, v any) error + // Transform runs the API transformers on the given value. The `status` is + // the key in the operation's `Responses` map that corresponds to the + // response being sent (e.g. "200" for a 200 OK response). + Transform(ctx Context, status string, v any) (any, error) + + // Marshal marshals the given value into the given writer. The + // content type is used to determine which format to use. Use `Negotiate` to + // get the content type from an accept header. + Marshal(w io.Writer, contentType string, v any) error // Unmarshal unmarshals the given data into the given value. The content type Unmarshal(contentType string, data []byte, v any) error @@ -193,16 +198,18 @@ func (a *api) Negotiate(accept string) (string, error) { return ct, nil } -func (a *api) Marshal(ctx Context, respKey string, ct string, v any) error { +func (a *api) Transform(ctx Context, status string, v any) (any, error) { var err error - for _, t := range a.transformers { - v, err = t(ctx, respKey, v) + v, err = t(ctx, status, v) if err != nil { - return err + return nil, err } } + return v, nil +} +func (a *api) Marshal(w io.Writer, ct string, v any) error { f, ok := a.formats[ct] if !ok { start := strings.IndexRune(ct, '+') + 1 @@ -211,7 +218,7 @@ func (a *api) Marshal(ctx Context, respKey string, ct string, v any) error { if !ok { return fmt.Errorf("unknown content type: %s", ct) } - return f.Marshal(ctx.BodyWriter(), v) + return f.Marshal(w, v) } func (a *api) UseMiddleware(middlewares ...func(ctx Context, next func(Context))) { diff --git a/api_test.go b/api_test.go index 5a642837..b4e3e4aa 100644 --- a/api_test.go +++ b/api_test.go @@ -1,16 +1,69 @@ -package huma +package huma_test import ( + "net/http" "testing" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/humatest" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" ) func TestBlankConfig(t *testing.T) { - adapter := &testAdapter{chi.NewMux()} + adapter := humatest.NewAdapter(chi.NewMux()) assert.NotPanics(t, func() { - NewAPI(Config{}, adapter) + huma.NewAPI(huma.Config{}, adapter) + }) +} + +// ExampleAdapter_handle demonstrates how to use the adapter directly +// instead of using the `huma.Register` convenience function to add a new +// operation and handler to the API. +// +// Note that you are responsible for defining all of the operation details, +// including the parameter and response definitions & schemas. +func ExampleAdapter_handle() { + // Create an adapter for your chosen router. + adapter := NewExampleAdapter(chi.NewMux()) + + // Register an operation with a custom handler. + adapter.Handle(&huma.Operation{ + OperationID: "example-operation", + Method: "GET", + Path: "/example/{name}", + Summary: "Example operation", + Parameters: []*huma.Param{ + { + Name: "name", + In: "path", + Description: "Name to return", + Required: true, + Schema: &huma.Schema{ + Type: "string", + }, + }, + }, + Responses: map[string]*huma.Response{ + "200": { + Description: "OK", + Content: map[string]*huma.MediaType{ + "text/plain": { + Schema: &huma.Schema{ + Type: "string", + }, + }, + }, + }, + }, + }, func(ctx huma.Context) { + // Get the `name` path parameter. + name := ctx.Param("name") + + // Set the response content type, status code, and body. + ctx.SetHeader("Content-Type", "text/plain; charset=utf-8") + ctx.SetStatus(http.StatusOK) + ctx.BodyWriter().Write([]byte("Hello, " + name)) }) } diff --git a/cli_test.go b/cli_test.go index c9094157..d5f094a2 100644 --- a/cli_test.go +++ b/cli_test.go @@ -1,15 +1,69 @@ -package huma +package huma_test import ( "bytes" + "context" + "fmt" + "log" + "net/http" "syscall" "testing" "time" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" + "github.com/go-chi/chi/v5" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) +func ExampleCLI() { + // First, define your input options. + type Options struct { + Debug bool `doc:"Enable debug logging"` + Host string `doc:"Hostname to listen on."` + Port int `doc:"Port to listen on." short:"p" default:"8888"` + } + + // Then, create the CLI. + cli := huma.NewCLI(func(hooks huma.Hooks, opts *Options) { + fmt.Printf("Options are debug:%v host:%v port%v\n", + opts.Debug, opts.Host, opts.Port) + + // Set up the router & API + router := chi.NewRouter() + api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0")) + + huma.Register(api, huma.Operation{ + OperationID: "hello", + Method: http.MethodGet, + Path: "/hello", + }, func(ctx context.Context, input *struct{}) (*struct{}, error) { + // TODO: implement handler + return nil, nil + }) + + srv := &http.Server{ + Addr: fmt.Sprintf("%s:%d", opts.Host, opts.Port), + Handler: router, + // TODO: Set up timeouts! + } + + hooks.OnStart(func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }) + + hooks.OnStop(func() { + srv.Shutdown(context.Background()) + }) + }) + + // Run the thing! + cli.Run() +} + func TestCLIPlain(t *testing.T) { type Options struct { Debug bool @@ -20,7 +74,7 @@ func TestCLIPlain(t *testing.T) { ingore bool } - cli := NewCLI(func(hooks Hooks, options *Options) { + cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) { assert.Equal(t, true, options.Debug) assert.Equal(t, "localhost", options.Host) assert.Equal(t, 8001, options.Port) @@ -46,7 +100,7 @@ func TestCLIAdvanced(t *testing.T) { Port int `doc:"Port to listen on." short:"p" default:"8000"` } - cli := NewCLI(func(hooks Hooks, options *Options) { + cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) { assert.Equal(t, true, options.Debug) assert.Equal(t, "localhost", options.Host) assert.Equal(t, 8001, options.Port) @@ -73,7 +127,7 @@ func TestCLIHelp(t *testing.T) { Port int } - cli := NewCLI(func(hooks Hooks, options *Options) { + cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) { // Do nothing }) @@ -91,14 +145,14 @@ func TestCLICommandWithOptions(t *testing.T) { Debug bool } - cli := NewCLI(func(hooks Hooks, options *Options) { + cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) { // Do nothing }) wasSet := false cli.Root().AddCommand(&cobra.Command{ Use: "custom", - Run: WithOptions(func(cmd *cobra.Command, args []string, options *Options) { + Run: huma.WithOptions(func(cmd *cobra.Command, args []string, options *Options) { if options.Debug { wasSet = true } @@ -116,7 +170,7 @@ func TestCLIShutdown(t *testing.T) { started := false stopping := make(chan bool, 1) - cli := NewCLI(func(hooks Hooks, options *Options) { + cli := huma.NewCLI(func(hooks huma.Hooks, options *Options) { hooks.OnStart(func() { started = true <-stopping @@ -142,7 +196,7 @@ func TestCLIBadType(t *testing.T) { } assert.Panics(t, func() { - NewCLI(func(hooks Hooks, options *Options) {}) + huma.NewCLI(func(hooks huma.Hooks, options *Options) {}) }) } @@ -156,10 +210,10 @@ func TestCLIBadDefaults(t *testing.T) { } assert.Panics(t, func() { - NewCLI(func(hooks Hooks, options *OptionsBool) {}) + huma.NewCLI(func(hooks huma.Hooks, options *OptionsBool) {}) }) assert.Panics(t, func() { - NewCLI(func(hooks Hooks, options *OptionsInt) {}) + huma.NewCLI(func(hooks huma.Hooks, options *OptionsInt) {}) }) } diff --git a/error.go b/error.go index 2474719f..5a85686b 100644 --- a/error.go +++ b/error.go @@ -217,7 +217,11 @@ func WriteErr(api API, ctx Context, status int, msg string, errs ...error) error ctx.SetHeader("Content-Type", ct) ctx.SetStatus(status) - return api.Marshal(ctx, strconv.Itoa(status), ct, err) + tval, terr := api.Transform(ctx, strconv.Itoa(status), err) + if terr != nil { + return terr + } + return api.Marshal(ctx.BodyWriter(), ct, tval) } // Status304NotModified returns a 304. This is not really an error, but diff --git a/error_test.go b/error_test.go index b6c78d74..6d5de750 100644 --- a/error_test.go +++ b/error_test.go @@ -1,4 +1,4 @@ -package huma +package huma_test import ( "fmt" @@ -6,23 +6,24 @@ import ( "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/humatest" "github.com/stretchr/testify/assert" ) // Ensure the default error models satisfy these interfaces. -var _ StatusError = (*ErrorModel)(nil) -var _ ContentTypeFilter = (*ErrorModel)(nil) -var _ ErrorDetailer = (*ErrorDetail)(nil) +var _ huma.StatusError = (*huma.ErrorModel)(nil) +var _ huma.ContentTypeFilter = (*huma.ErrorModel)(nil) +var _ huma.ErrorDetailer = (*huma.ErrorDetail)(nil) func TestError(t *testing.T) { - err := &ErrorModel{ + err := &huma.ErrorModel{ Status: 400, Detail: "test err", } // Add some children. - err.Add(&ErrorDetail{ + err.Add(&huma.ErrorDetail{ Message: "test detail", Location: "body.foo", Value: "bar", @@ -44,29 +45,29 @@ func TestError(t *testing.T) { func TestErrorResponses(t *testing.T) { // NotModified has a slightly different signature. - assert.Equal(t, 304, Status304NotModified().GetStatus()) + assert.Equal(t, 304, huma.Status304NotModified().GetStatus()) for _, item := range []struct { - constructor func(msg string, errs ...error) StatusError + constructor func(msg string, errs ...error) huma.StatusError expected int }{ - {Error400BadRequest, 400}, - {Error401Unauthorized, 401}, - {Error403Forbidden, 403}, - {Error404NotFound, 404}, - {Error405MethodNotAllowed, 405}, - {Error406NotAcceptable, 406}, - {Error409Conflict, 409}, - {Error410Gone, 410}, - {Error412PreconditionFailed, 412}, - {Error415UnsupportedMediaType, 415}, - {Error422UnprocessableEntity, 422}, - {Error429TooManyRequests, 429}, - {Error500InternalServerError, 500}, - {Error501NotImplemented, 501}, - {Error502BadGateway, 502}, - {Error503ServiceUnavailable, 503}, - {Error504GatewayTimeout, 504}, + {huma.Error400BadRequest, 400}, + {huma.Error401Unauthorized, 401}, + {huma.Error403Forbidden, 403}, + {huma.Error404NotFound, 404}, + {huma.Error405MethodNotAllowed, 405}, + {huma.Error406NotAcceptable, 406}, + {huma.Error409Conflict, 409}, + {huma.Error410Gone, 410}, + {huma.Error412PreconditionFailed, 412}, + {huma.Error415UnsupportedMediaType, 415}, + {huma.Error422UnprocessableEntity, 422}, + {huma.Error429TooManyRequests, 429}, + {huma.Error500InternalServerError, 500}, + {huma.Error501NotImplemented, 501}, + {huma.Error502BadGateway, 502}, + {huma.Error503ServiceUnavailable, 503}, + {huma.Error504GatewayTimeout, 504}, } { err := item.constructor("test") assert.Equal(t, item.expected, err.GetStatus()) @@ -74,12 +75,11 @@ func TestErrorResponses(t *testing.T) { } func TestNegotiateError(t *testing.T) { - r := chi.NewMux() - api := NewTestAdapter(r, Config{}) + _, api := humatest.New(t, huma.Config{}) req, _ := http.NewRequest("GET", "/", nil) resp := httptest.NewRecorder() - ctx := &testContext{nil, req, resp} + ctx := humatest.NewContext(nil, req, resp) - assert.Error(t, WriteErr(api, ctx, 400, "bad request")) + assert.Error(t, huma.WriteErr(api, ctx, 400, "bad request")) } diff --git a/huma.go b/huma.go index f3d6c41f..e60839e8 100644 --- a/huma.go +++ b/huma.go @@ -356,6 +356,23 @@ var bufPool = sync.Pool{ }, } +// transformAndWrite is a utility function to transform and write a response. +// It is best-effort as the status code and headers may have already been sent. +func transformAndWrite(api API, ctx Context, status int, ct string, body any) { + // Try to transform and then marshal/write the response. + // Status code was already sent, so just log the error if something fails, + // and do our best to stuff it into the body of the response. + tval, terr := api.Transform(ctx, strconv.Itoa(status), body) + if terr != nil { + ctx.BodyWriter().Write([]byte("error transforming response")) + panic(fmt.Sprintf("error transforming response %+v for %s %s %d: %s\n", tval, ctx.Operation().Method, ctx.Operation().Path, status, terr.Error())) + } + if merr := api.Marshal(ctx.BodyWriter(), ct, tval); merr != nil { + ctx.BodyWriter().Write([]byte("error marshaling response")) + panic(fmt.Sprintf("error marshaling response %+v for %s %s %d: %s\n", tval, ctx.Operation().Method, ctx.Operation().Path, status, merr.Error())) + } +} + // Register an operation handler for an API. The handler must be a function that // takes a context and a pointer to the input struct and returns a pointer to the // output struct and an error. The input struct must be a struct with fields @@ -835,7 +852,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) ctx.SetStatus(status) ctx.SetHeader("Content-Type", ct) - api.Marshal(ctx, strconv.Itoa(status), ct, err) + transformAndWrite(api, ctx, status, ct, err) return } @@ -903,7 +920,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) } ctx.SetStatus(status) - api.Marshal(ctx, strconv.Itoa(op.DefaultStatus), ct, body) + transformAndWrite(api, ctx, status, ct, body) } else { ctx.SetStatus(status) } @@ -915,9 +932,9 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) // passed the API as the only argument. Since registration happens at // service startup, no errors are returned and methods should panic on error. // -// type ItemServer struct {} +// type ItemsHandler struct {} // -// func (s *ItemServer) RegisterListItems(api API) { +// func (s *ItemsHandler) RegisterListItems(api API) { // huma.Register(api, huma.Operation{ // OperationID: "ListItems", // Method: http.MethodGet, @@ -926,9 +943,12 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) // } // // func main() { -// api := huma.NewAPI("My Service", "1.0.0") -// itemServer := &ItemServer{} -// huma.AutoRegister(api, itemServer) +// router := chi.NewMux() +// config := huma.DefaultConfig("My Service", "1.0.0") +// api := huma.NewExampleAPI(router, config) +// +// itemsHandler := &ItemsHandler{} +// huma.AutoRegister(api, itemsHandler) // } func AutoRegister(api API, server any) { args := []reflect.Value{reflect.ValueOf(server), reflect.ValueOf(api)} diff --git a/huma_test.go b/huma_test.go index 83044d90..3d0d6c24 100644 --- a/huma_test.go +++ b/huma_test.go @@ -1,141 +1,85 @@ -package huma +package huma_test import ( "context" "encoding/json" "fmt" "io" - "mime/multipart" "net/http" "net/http/httptest" - "net/url" "reflect" "strings" "testing" "time" - "github.com/danielgtaylor/huma/v2/queryparam" + "github.com/danielgtaylor/huma/v2" + "github.com/danielgtaylor/huma/v2/adapters/humachi" + "github.com/danielgtaylor/huma/v2/humatest" "github.com/go-chi/chi/v5" "github.com/goccy/go-yaml" "github.com/mitchellh/mapstructure" "github.com/stretchr/testify/assert" ) -type testContext struct { - op *Operation - r *http.Request - w http.ResponseWriter -} - -func (c *testContext) Operation() *Operation { - return c.op -} - -func (c *testContext) Matched() string { - return chi.RouteContext(c.r.Context()).RoutePattern() -} - -func (c *testContext) Context() context.Context { - return c.r.Context() -} - -func (c *testContext) Method() string { - return c.r.Method -} +var NewExampleAdapter = humatest.NewAdapter +var NewExampleAPI = humachi.New -func (c *testContext) Host() string { - return c.r.Host -} - -func (c *testContext) URL() url.URL { - return *c.r.URL -} - -func (c *testContext) Param(name string) string { - return chi.URLParam(c.r, name) -} - -func (c *testContext) Query(name string) string { - return queryparam.Get(c.r.URL.RawQuery, name) -} - -func (c *testContext) Header(name string) string { - return c.r.Header.Get(name) -} +// Recoverer is a really simple recovery middleware we can use during tests. +func Recoverer(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rvr := recover(); rvr != nil { + w.WriteHeader(http.StatusInternalServerError) + } + }() -func (c *testContext) EachHeader(cb func(name, value string)) { - for name, values := range c.r.Header { - for _, value := range values { - cb(name, value) - } + next.ServeHTTP(w, r) } -} - -func (c *testContext) Body() ([]byte, error) { - return io.ReadAll(c.r.Body) -} -func (c *testContext) BodyReader() io.Reader { - return c.r.Body -} - -func (c *testContext) GetMultipartForm() (*multipart.Form, error) { - err := c.r.ParseMultipartForm(8 * 1024) - return c.r.MultipartForm, err -} - -func (c *testContext) SetReadDeadline(deadline time.Time) error { - return http.NewResponseController(c.w).SetReadDeadline(deadline) -} - -func (c *testContext) SetStatus(code int) { - c.w.WriteHeader(code) -} - -func (c *testContext) AppendHeader(name string, value string) { - c.w.Header().Add(name, value) -} - -func (c *testContext) SetHeader(name string, value string) { - c.w.Header().Set(name, value) -} - -func (c *testContext) BodyWriter() io.Writer { - return c.w -} - -type testAdapter struct { - router chi.Router -} - -func (a *testAdapter) Handle(op *Operation, handler func(Context)) { - a.router.MethodFunc(op.Method, op.Path, func(w http.ResponseWriter, r *http.Request) { - handler(&testContext{op: op, r: r, w: w}) - }) -} - -func (a *testAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - a.router.ServeHTTP(w, r) -} - -func NewTestAdapter(r chi.Router, config Config) API { - return NewAPI(config, &testAdapter{router: r}) + return http.HandlerFunc(fn) } func TestFeatures(t *testing.T) { for _, feature := range []struct { - Name string - Register func(t *testing.T, api API) - Method string - URL string - Headers map[string]string - Body string - Assert func(t *testing.T, resp *httptest.ResponseRecorder) + Name string + Transformers []huma.Transformer + Register func(t *testing.T, api huma.API) + Method string + URL string + Headers map[string]string + Body string + Assert func(t *testing.T, resp *httptest.ResponseRecorder) }{ + { + Name: "middleware", + Register: func(t *testing.T, api huma.API) { + api.UseMiddleware(func(ctx huma.Context, next func(huma.Context)) { + // Just a do-nothing passthrough. Shows that chaining works. + next(ctx) + }) + api.UseMiddleware(func(ctx huma.Context, next func(huma.Context)) { + // Return an error response, never calling the next handler. + ctx.SetStatus(299) + }) + huma.Register(api, huma.Operation{ + Method: http.MethodGet, + Path: "/middleware", + }, func(ctx context.Context, input *struct{}) (*struct{}, error) { + // This should never be called because of the middleware. + return nil, nil + }) + }, + Method: http.MethodGet, + URL: "/middleware", + Assert: func(t *testing.T, resp *httptest.ResponseRecorder) { + // We should get the error response from the middleware. + assert.Equal(t, 299, resp.Code) + }, + }, { Name: "params", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/test-params/{string}/{int}", }, func(ctx context.Context, input *struct { @@ -178,8 +122,8 @@ func TestFeatures(t *testing.T) { }, { Name: "params-error", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/test-params/{int}", }, func(ctx context.Context, input *struct { @@ -210,8 +154,8 @@ func TestFeatures(t *testing.T) { }, { Name: "request-body", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/body", }, func(ctx context.Context, input *struct { @@ -232,8 +176,8 @@ func TestFeatures(t *testing.T) { }, { Name: "request-body-required", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/body", }, func(ctx context.Context, input *struct { @@ -252,8 +196,8 @@ func TestFeatures(t *testing.T) { }, { Name: "request-ptr-body-required", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/body", }, func(ctx context.Context, input *struct { @@ -272,8 +216,8 @@ func TestFeatures(t *testing.T) { }, { Name: "request-body-too-large", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/body", MaxBodyBytes: 1, @@ -294,8 +238,8 @@ func TestFeatures(t *testing.T) { }, { Name: "request-body-bad-json", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/body", }, func(ctx context.Context, input *struct { @@ -315,8 +259,8 @@ func TestFeatures(t *testing.T) { }, { Name: "request-body-file-upload", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/file", }, func(ctx context.Context, input *struct { @@ -337,12 +281,12 @@ func TestFeatures(t *testing.T) { }, { Name: "handler-error", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/error", }, func(ctx context.Context, input *struct{}) (*struct{}, error) { - return nil, Error403Forbidden("nope") + return nil, huma.Error403Forbidden("nope") }) }, Method: http.MethodGet, @@ -353,7 +297,7 @@ func TestFeatures(t *testing.T) { }, { Name: "response-headers", - Register: func(t *testing.T, api API) { + Register: func(t *testing.T, api huma.API) { type Resp struct { Str string `header:"str"` Int int `header:"int"` @@ -363,7 +307,7 @@ func TestFeatures(t *testing.T) { Date time.Time `header:"date"` } - Register(api, Operation{ + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/response-headers", }, func(ctx context.Context, input *struct{}) (*Resp, error) { @@ -391,14 +335,14 @@ func TestFeatures(t *testing.T) { }, { Name: "response", - Register: func(t *testing.T, api API) { + Register: func(t *testing.T, api huma.API) { type Resp struct { Body struct { Greeting string `json:"greeting"` } } - Register(api, Operation{ + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/response", }, func(ctx context.Context, input *struct{}) (*Resp, error) { @@ -416,12 +360,12 @@ func TestFeatures(t *testing.T) { }, { Name: "response-raw", - Register: func(t *testing.T, api API) { + Register: func(t *testing.T, api huma.API) { type Resp struct { Body []byte } - Register(api, Operation{ + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/response-raw", }, func(ctx context.Context, input *struct{}) (*Resp, error) { @@ -437,13 +381,13 @@ func TestFeatures(t *testing.T) { }, { Name: "response-stream", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/stream", - }, func(ctx context.Context, input *struct{}) (*StreamResponse, error) { - return &StreamResponse{ - Body: func(ctx Context) { + }, func(ctx context.Context, input *struct{}) (*huma.StreamResponse, error) { + return &huma.StreamResponse{ + Body: func(ctx huma.Context) { writer := ctx.BodyWriter() writer.Write([]byte("hel")) writer.Write([]byte("lo")) @@ -458,14 +402,64 @@ func TestFeatures(t *testing.T) { assert.Equal(t, `hello`, resp.Body.String()) }, }, + { + Name: "response-transform-error", + Transformers: []huma.Transformer{ + func(ctx huma.Context, status string, v any) (any, error) { + return nil, fmt.Errorf("whoops") + }, + }, + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ + Method: http.MethodGet, + Path: "/response", + }, func(ctx context.Context, input *struct{}) (*struct{ Body string }, error) { + return &struct{ Body string }{"foo"}, nil + }) + }, + Method: http.MethodGet, + URL: "/response", + Assert: func(t *testing.T, resp *httptest.ResponseRecorder) { + // Since the handler completed, this returns a 204, however while + // writing the body there is an error, so that is written as a message + // into the body and dumped via a panic. + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, `error transforming response`, resp.Body.String()) + }, + }, + { + Name: "response-marshal-error", + Register: func(t *testing.T, api huma.API) { + type Resp struct { + Body struct { + Greeting any `json:"greeting"` + } + } + + huma.Register(api, huma.Operation{ + Method: http.MethodGet, + Path: "/response", + }, func(ctx context.Context, input *struct{}) (*Resp, error) { + resp := &Resp{} + resp.Body.Greeting = func() {} + return resp, nil + }) + }, + Method: http.MethodGet, + URL: "/response", + Assert: func(t *testing.T, resp *httptest.ResponseRecorder) { + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, `error marshaling response`, resp.Body.String()) + }, + }, { Name: "dynamic-status", - Register: func(t *testing.T, api API) { + Register: func(t *testing.T, api huma.API) { type Resp struct { Status int } - Register(api, Operation{ + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/status", }, func(ctx context.Context, input *struct{}) (*Resp, error) { @@ -485,8 +479,8 @@ func TestFeatures(t *testing.T) { // includes the `$schema` field. It should be allowed to be passed // to the new operation as input without modification. Name: "round-trip-schema-field", - Register: func(t *testing.T, api API) { - Register(api, Operation{ + Register: func(t *testing.T, api huma.API) { + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/round-trip", }, func(ctx context.Context, input *struct { @@ -503,22 +497,22 @@ func TestFeatures(t *testing.T) { }, { Name: "one-of input", - Register: func(t *testing.T, api API) { + Register: func(t *testing.T, api huma.API) { // Step 1: create a custom schema - customSchema := &Schema{ - OneOf: []*Schema{ + customSchema := &huma.Schema{ + OneOf: []*huma.Schema{ { - Type: TypeObject, - Properties: map[string]*Schema{ - "foo": {Type: TypeString}, + Type: huma.TypeObject, + Properties: map[string]*huma.Schema{ + "foo": {Type: huma.TypeString}, }, }, { - Type: TypeArray, - Items: &Schema{ - Type: TypeObject, - Properties: map[string]*Schema{ - "foo": {Type: TypeString}, + Type: huma.TypeArray, + Items: &huma.Schema{ + Type: huma.TypeObject, + Properties: map[string]*huma.Schema{ + "foo": {Type: huma.TypeString}, }, }, }, @@ -526,13 +520,13 @@ func TestFeatures(t *testing.T) { } customSchema.PrecomputeMessages() - Register(api, Operation{ + huma.Register(api, huma.Operation{ Method: http.MethodPut, Path: "/one-of", // Step 2: register an operation with a custom schema - RequestBody: &RequestBody{ + RequestBody: &huma.RequestBody{ Required: true, - Content: map[string]*MediaType{ + Content: map[string]*huma.MediaType{ "application/json": { Schema: customSchema, }, @@ -562,7 +556,12 @@ func TestFeatures(t *testing.T) { } { t.Run(feature.Name, func(t *testing.T) { r := chi.NewRouter() - api := NewTestAdapter(r, DefaultConfig("Features Test API", "1.0.0")) + r.Use(Recoverer) + config := huma.DefaultConfig("Features Test API", "1.0.0") + if feature.Transformers != nil { + config.Transformers = append(config.Transformers, feature.Transformers...) + } + api := humatest.NewTestAPI(t, r, config) feature.Register(t, api) var body io.Reader = nil @@ -587,8 +586,7 @@ func TestFeatures(t *testing.T) { } func TestOpenAPI(t *testing.T) { - r := chi.NewRouter() - api := NewTestAdapter(r, DefaultConfig("Features Test API", "1.0.0")) + r, api := humatest.New(t, huma.DefaultConfig("Features Test API", "1.0.0")) type Resp struct { Body struct { @@ -596,7 +594,7 @@ func TestOpenAPI(t *testing.T) { } } - Register(api, Operation{ + huma.Register(api, huma.Operation{ Method: http.MethodGet, Path: "/test", }, func(ctx context.Context, input *struct{}) (*Resp, error) { @@ -619,7 +617,7 @@ type ExhaustiveErrorsInputBody struct { Count int `json:"count" minimum:"1"` } -func (b *ExhaustiveErrorsInputBody) Resolve(ctx Context) []error { +func (b *ExhaustiveErrorsInputBody) Resolve(ctx huma.Context) []error { return []error{fmt.Errorf("body resolver error")} } @@ -628,8 +626,8 @@ type ExhaustiveErrorsInput struct { Body ExhaustiveErrorsInputBody `json:"body"` } -func (i *ExhaustiveErrorsInput) Resolve(ctx Context) []error { - return []error{&ErrorDetail{ +func (i *ExhaustiveErrorsInput) Resolve(ctx huma.Context) []error { + return []error{&huma.ErrorDetail{ Location: "path.id", Message: "input resolver error", Value: i.ID, @@ -640,9 +638,8 @@ type ExhaustiveErrorsOutput struct { } func TestExhaustiveErrors(t *testing.T) { - r := chi.NewRouter() - app := NewTestAdapter(r, DefaultConfig("Test API", "1.0.0")) - Register(app, Operation{ + r, app := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0")) + huma.Register(app, huma.Operation{ OperationID: "test", Method: http.MethodPut, Path: "/errors/{id}", @@ -688,15 +685,15 @@ type NestedResolversStruct struct { Field2 string `json:"field2"` } -func (b *NestedResolversStruct) Resolve(ctx Context, prefix *PathBuffer) []error { - return []error{&ErrorDetail{ +func (b *NestedResolversStruct) Resolve(ctx huma.Context, prefix *huma.PathBuffer) []error { + return []error{&huma.ErrorDetail{ Location: prefix.With("field2"), Message: "resolver error", Value: b.Field2, }} } -var _ ResolverWithPath = (*NestedResolversStruct)(nil) +var _ huma.ResolverWithPath = (*NestedResolversStruct)(nil) type NestedResolversBody struct { Field1 map[string][]NestedResolversStruct `json:"field1"` @@ -707,9 +704,8 @@ type NestedResolverRequest struct { } func TestNestedResolverWithPath(t *testing.T) { - r := chi.NewRouter() - app := NewTestAdapter(r, DefaultConfig("Test API", "1.0.0")) - Register(app, Operation{ + r, app := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0")) + huma.Register(app, huma.Operation{ OperationID: "test", Method: http.MethodPut, Path: "/test", @@ -727,14 +723,13 @@ func TestNestedResolverWithPath(t *testing.T) { type ResolverCustomStatus struct{} -func (r *ResolverCustomStatus) Resolve(ctx Context) []error { - return []error{Error403Forbidden("nope")} +func (r *ResolverCustomStatus) Resolve(ctx huma.Context) []error { + return []error{huma.Error403Forbidden("nope")} } func TestResolverCustomStatus(t *testing.T) { - r := chi.NewRouter() - app := NewTestAdapter(r, DefaultConfig("Test API", "1.0.0")) - Register(app, Operation{ + r, app := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0")) + huma.Register(app, huma.Operation{ OperationID: "test", Method: http.MethodPut, Path: "/test", @@ -798,9 +793,9 @@ func BenchmarkSecondDecode(b *testing.B) { ] }`) - pb := NewPathBuffer([]byte{}, 0) - res := &ValidateResult{} - registry := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + pb := huma.NewPathBuffer([]byte{}, 0) + res := &huma.ValidateResult{} + registry := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) fmt.Println("name", reflect.TypeOf(MediumSized{}).Name()) schema := registry.Schema(reflect.TypeOf(MediumSized{}), false, "") @@ -812,7 +807,7 @@ func BenchmarkSecondDecode(b *testing.B) { panic(err) } - Validate(registry, schema, pb, ModeReadFromServer, tmp, res) + huma.Validate(registry, schema, pb, huma.ModeReadFromServer, tmp, res) var out MediumSized if err := json.Unmarshal(data, &out); err != nil { @@ -829,7 +824,7 @@ func BenchmarkSecondDecode(b *testing.B) { panic(err) } - Validate(registry, schema, pb, ModeReadFromServer, tmp, res) + huma.Validate(registry, schema, pb, huma.ModeReadFromServer, tmp, res) var out MediumSized if err := mapstructure.Decode(tmp, &out); err != nil { @@ -838,169 +833,3 @@ func BenchmarkSecondDecode(b *testing.B) { } }) } - -// var jsonData = []byte(`[ -// { -// "desired_state": "ON", -// "etag": "203f7a94", -// "id": "bvt3", -// "name": "BVT channel - CNN Plus 2", -// "org": "t2dev", -// "self": "https://api.istreamplanet.com/v2/t2dev/channels/bvt3", -// "created": "2021-01-01T12:00:00Z", -// "count": 18273, -// "rating": 5.0, -// "tags": ["one", "three"], -// "source": { -// "id": "stn-dd4j42ytxmajz6xz", -// "self": "https://api.istreamplanet.com/v2/t2dev/sources/stn-dd4j42ytxmajz6xz" -// } -// }, -// { -// "desired_state": "ON", -// "etag": "WgY5zNTPn3ECf_TSPAgL9Y-E9doUaRxAdjukGsCt_sQ", -// "id": "bvt2", -// "name": "BVT channel - Hulu", -// "org": "t2dev", -// "self": "https://api.istreamplanet.com/v2/t2dev/channels/bvt2", -// "created": "2023-01-01T12:01:00Z", -// "count": 1, -// "rating": 4.5, -// "tags": ["two"], -// "source": { -// "id": "stn-yuqvm3hzowrv6rph", -// "self": "https://api.istreamplanet.com/v2/t2dev/sources/stn-yuqvm3hzowrv6rph" -// } -// }, -// { -// "desired_state": "ON", -// "etag": "1GaleyULVhpmHJXCJPUGSeBM2YYAZGBYKVcR5sZu5U8", -// "id": "bvt1", -// "name": "BVT channel - Hulu", -// "org": "t2dev", -// "self": "https://api.istreamplanet.com/v2/t2dev/channels/bvt1", -// "created": "2023-01-01T12:00:00Z", -// "count": 57, -// "rating": 3.5, -// "tags": ["one", "two"], -// "source": { -// "id": "stn-fc6sqodptbz5keuy", -// "self": "https://api.istreamplanet.com/v2/t2dev/sources/stn-fc6sqodptbz5keuy" -// } -// } -// ]`) - -// type Summary struct { -// DesiredState string `json:"desired_state"` -// ETag string `json:"etag"` -// ID string `json:"id"` -// Name string `json:"name"` -// Org string `json:"org"` -// Self string `json:"self"` -// Created time.Time `json:"created"` -// Count int `json:"count"` -// Rating float64 `json:"rating"` -// Tags []string `json:"tags"` -// Source struct { -// ID string `json:"id"` -// Self string `json:"self"` -// } `json:"source"` -// } - -// func BenchmarkMarshalStructJSON(b *testing.B) { -// var summaries []Summary -// if err := stdjson.Unmarshal(jsonData, &summaries); err != nil { -// panic(err) -// } - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// b, _ := stdjson.Marshal(summaries) -// _ = b -// } -// } - -// func BenchmarkMarshalAnyJSON(b *testing.B) { -// var summaries any -// stdjson.Unmarshal(jsonData, &summaries) - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// b, _ := stdjson.Marshal(summaries) -// _ = b -// } -// } - -// func BenchmarkUnmarshalStructJSON(b *testing.B) { -// var summaries []Summary - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// summaries = nil -// stdjson.Unmarshal(jsonData, &summaries) -// _ = summaries -// } -// } - -// func BenchmarkUnmarshalAnyJSON(b *testing.B) { -// var summaries any - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// summaries = nil -// stdjson.Unmarshal(jsonData, &summaries) -// _ = summaries -// } -// } - -// func BenchmarkMarshalStructJSONiter(b *testing.B) { -// var summaries []Summary -// json.Unmarshal(jsonData, &summaries) - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// b, _ := json.Marshal(summaries) -// _ = b -// } -// } - -// func BenchmarkMarshalAnyJSONiter(b *testing.B) { -// var summaries any -// json.Unmarshal(jsonData, &summaries) - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// b, _ := json.Marshal(summaries) -// _ = b -// } -// } - -// func BenchmarkUnmarshalStructJSONiter(b *testing.B) { -// var summaries []Summary - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// summaries = nil -// json.Unmarshal(jsonData, &summaries) -// _ = summaries -// } -// } - -// func BenchmarkUnmarshalAnyJSONiter(b *testing.B) { -// var summaries any - -// b.ResetTimer() -// b.ReportAllocs() -// for i := 0; i < b.N; i++ { -// summaries = nil -// json.Unmarshal(jsonData, &summaries) -// _ = summaries -// } -// } diff --git a/schema_test.go b/schema_test.go index 693d4914..e50dfb60 100644 --- a/schema_test.go +++ b/schema_test.go @@ -1,4 +1,4 @@ -package huma +package huma_test import ( "bytes" @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/danielgtaylor/huma/v2" "github.com/stretchr/testify/assert" ) @@ -431,7 +432,7 @@ func TestSchema(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) if c.panics != "" { assert.PanicsWithError(t, c.panics, func() { @@ -464,7 +465,7 @@ type RecursiveInput struct { } func TestSchemaOld(t *testing.T) { - r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) s := r.Schema(reflect.TypeOf(GreetingInput{}), false, "") // fmt.Printf("%+v\n", s) @@ -475,9 +476,9 @@ func TestSchemaOld(t *testing.T) { r.Schema(reflect.TypeOf(RecursiveInput{}), false, "") s2 := r.Schema(reflect.TypeOf(TestInput{}), false, "") - pb := NewPathBuffer(make([]byte, 0, 128), 0) - res := ValidateResult{} - Validate(r, s2, pb, ModeReadFromServer, map[string]any{ + pb := huma.NewPathBuffer(make([]byte, 0, 128), 0) + res := huma.ValidateResult{} + huma.Validate(r, s2, pb, huma.ModeReadFromServer, map[string]any{ "name": "foo", "sub": map[string]any{ "num": 1.0, @@ -494,7 +495,7 @@ func TestSchemaGenericNaming(t *testing.T) { Value T `json:"value"` } - r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) s := r.Schema(reflect.TypeOf(SchemaGeneric[int]{}), true, "") b, _ := json.Marshal(s) @@ -521,7 +522,7 @@ func (o *OmittableNullable[T]) UnmarshalJSON(b []byte) error { return nil } -func (o OmittableNullable[T]) Schema(r Registry) *Schema { +func (o OmittableNullable[T]) Schema(r huma.Registry) *huma.Schema { return r.Schema(reflect.TypeOf(o.Value), true, "") } @@ -533,7 +534,7 @@ func TestCustomUnmarshalType(t *testing.T) { var o O // Confirm the schema is generated properly, including field constraints. - r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) s := r.Schema(reflect.TypeOf(o), false, "") assert.Equal(t, "integer", s.Properties["field"].Type, s) assert.Equal(t, Ptr(float64(10)), s.Properties["field"].Maximum, s) @@ -577,7 +578,7 @@ type BenchStruct struct { } func BenchmarkSchema(b *testing.B) { - r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) s2 := r.Schema(reflect.TypeOf(BenchStruct{}), false, "") @@ -596,9 +597,9 @@ func BenchmarkSchema(b *testing.B) { "metrics": []any{1.0, 2.0, 3.0}, }, } - pb := NewPathBuffer(make([]byte, 0, 128), 0) - res := ValidateResult{} - Validate(r, s2, pb, ModeReadFromServer, input, &res) + pb := huma.NewPathBuffer(make([]byte, 0, 128), 0) + res := huma.ValidateResult{} + huma.Validate(r, s2, pb, huma.ModeReadFromServer, input, &res) assert.Empty(b, res.Errors) b.ResetTimer() @@ -606,7 +607,7 @@ func BenchmarkSchema(b *testing.B) { for i := 0; i < b.N; i++ { pb.Reset() res.Reset() - Validate(r, s2, pb, ModeReadFromServer, input, &res) + huma.Validate(r, s2, pb, huma.ModeReadFromServer, input, &res) if len(res.Errors) > 0 { b.Fatal(res.Errors) } @@ -614,7 +615,7 @@ func BenchmarkSchema(b *testing.B) { } func BenchmarkSchemaErrors(b *testing.B) { - r := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + r := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) s2 := r.Schema(reflect.TypeOf(BenchStruct{}), false, "") @@ -630,9 +631,9 @@ func BenchmarkSchemaErrors(b *testing.B) { "unexpected": 2, }, } - pb := NewPathBuffer(make([]byte, 0, 128), 0) - res := ValidateResult{} - Validate(r, s2, pb, ModeReadFromServer, input, &res) + pb := huma.NewPathBuffer(make([]byte, 0, 128), 0) + res := huma.ValidateResult{} + huma.Validate(r, s2, pb, huma.ModeReadFromServer, input, &res) assert.NotEmpty(b, res.Errors) b.ResetTimer() @@ -640,7 +641,7 @@ func BenchmarkSchemaErrors(b *testing.B) { for i := 0; i < b.N; i++ { pb.Reset() res.Reset() - Validate(r, s2, pb, ModeReadFromServer, input, &res) + huma.Validate(r, s2, pb, huma.ModeReadFromServer, input, &res) if len(res.Errors) == 0 { b.Fatal("expected error") } diff --git a/validate_test.go b/validate_test.go index 15e575a8..17d07f7e 100644 --- a/validate_test.go +++ b/validate_test.go @@ -1,4 +1,4 @@ -package huma +package huma_test import ( "encoding/json" @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/danielgtaylor/huma/v2" "github.com/stretchr/testify/assert" ) @@ -15,12 +16,20 @@ func Ptr[T any](v T) *T { return &v } +func mapTo[A, B any](s []A, f func(A) B) []B { + r := make([]B, len(s)) + for i, v := range s { + r[i] = f(v) + } + return r +} + var validateTests = []struct { name string typ reflect.Type - s *Schema + s *huma.Schema input any - mode ValidateMode + mode huma.ValidateMode errs []string panic string }{ @@ -646,7 +655,7 @@ var validateTests = []struct { typ: reflect.TypeOf(struct { Value string `json:"value" readOnly:"true"` }{}), - mode: ModeWriteToServer, + mode: huma.ModeWriteToServer, input: map[string]any{"value": "whoops"}, }, { @@ -654,7 +663,7 @@ var validateTests = []struct { typ: reflect.TypeOf(struct { Value string `json:"value" readOnly:"true"` }{}), - mode: ModeWriteToServer, + mode: huma.ModeWriteToServer, input: map[string]any{}, }, { @@ -662,7 +671,7 @@ var validateTests = []struct { typ: reflect.TypeOf(struct { Value string `json:"value" readOnly:"true"` }{}), - mode: ModeReadFromServer, + mode: huma.ModeReadFromServer, input: map[string]any{}, errs: []string{"expected required property value to be present"}, }, @@ -671,7 +680,7 @@ var validateTests = []struct { typ: reflect.TypeOf(struct { Value string `json:"value" writeOnly:"true"` }{}), - mode: ModeReadFromServer, + mode: huma.ModeReadFromServer, input: map[string]any{"value": "should not be set"}, errs: []string{"write only property is non-zero"}, }, @@ -734,30 +743,30 @@ var validateTests = []struct { }, { name: "oneOf success bool", - s: &Schema{ - OneOf: []*Schema{ - {Type: TypeBoolean}, - {Type: TypeString}, + s: &huma.Schema{ + OneOf: []*huma.Schema{ + {Type: huma.TypeBoolean}, + {Type: huma.TypeString}, }, }, input: true, }, { name: "oneOf success string", - s: &Schema{ - OneOf: []*Schema{ - {Type: TypeBoolean}, - {Type: TypeString}, + s: &huma.Schema{ + OneOf: []*huma.Schema{ + {Type: huma.TypeBoolean}, + {Type: huma.TypeString}, }, }, input: "hello", }, { name: "oneOf fail zero", - s: &Schema{ - OneOf: []*Schema{ - {Type: TypeBoolean}, - {Type: TypeString}, + s: &huma.Schema{ + OneOf: []*huma.Schema{ + {Type: huma.TypeBoolean}, + {Type: huma.TypeString}, }, }, input: 123, @@ -765,10 +774,10 @@ var validateTests = []struct { }, { name: "oneOf fail multi", - s: &Schema{ - OneOf: []*Schema{ - {Type: TypeNumber, Minimum: Ptr(float64(5))}, - {Type: TypeNumber, Maximum: Ptr(float64(10))}, + s: &huma.Schema{ + OneOf: []*huma.Schema{ + {Type: huma.TypeNumber, Minimum: Ptr(float64(5))}, + {Type: huma.TypeNumber, Maximum: Ptr(float64(10))}, }, }, input: 8, @@ -776,20 +785,20 @@ var validateTests = []struct { }, { name: "anyOf success", - s: &Schema{ - AnyOf: []*Schema{ - {Type: TypeNumber, Minimum: Ptr(float64(5))}, - {Type: TypeNumber, Maximum: Ptr(float64(10))}, + s: &huma.Schema{ + AnyOf: []*huma.Schema{ + {Type: huma.TypeNumber, Minimum: Ptr(float64(5))}, + {Type: huma.TypeNumber, Maximum: Ptr(float64(10))}, }, }, input: 8, }, { name: "anyOf fail", - s: &Schema{ - AnyOf: []*Schema{ - {Type: TypeNumber, Minimum: Ptr(float64(5))}, - {Type: TypeNumber, Minimum: Ptr(float64(10))}, + s: &huma.Schema{ + AnyOf: []*huma.Schema{ + {Type: huma.TypeNumber, Minimum: Ptr(float64(5))}, + {Type: huma.TypeNumber, Minimum: Ptr(float64(10))}, }, }, input: 1, @@ -797,20 +806,20 @@ var validateTests = []struct { }, { name: "allOf success", - s: &Schema{ - AllOf: []*Schema{ - {Type: TypeNumber, Minimum: Ptr(float64(5))}, - {Type: TypeNumber, Maximum: Ptr(float64(10))}, + s: &huma.Schema{ + AllOf: []*huma.Schema{ + {Type: huma.TypeNumber, Minimum: Ptr(float64(5))}, + {Type: huma.TypeNumber, Maximum: Ptr(float64(10))}, }, }, input: 8, }, { name: "allOf fail", - s: &Schema{ - AllOf: []*Schema{ - {Type: TypeNumber, Minimum: Ptr(float64(5))}, - {Type: TypeNumber, Maximum: Ptr(float64(10))}, + s: &huma.Schema{ + AllOf: []*huma.Schema{ + {Type: huma.TypeNumber, Minimum: Ptr(float64(5))}, + {Type: huma.TypeNumber, Maximum: Ptr(float64(10))}, }, }, input: 12, @@ -818,15 +827,15 @@ var validateTests = []struct { }, { name: "not success", - s: &Schema{ - Not: &Schema{Type: TypeNumber}, + s: &huma.Schema{ + Not: &huma.Schema{Type: huma.TypeNumber}, }, input: "hello", }, { name: "not fail", - s: &Schema{ - Not: &Schema{Type: TypeNumber}, + s: &huma.Schema{ + Not: &huma.Schema{Type: huma.TypeNumber}, }, input: 5, errs: []string{"expected value to not match schema"}, @@ -834,14 +843,14 @@ var validateTests = []struct { } func TestValidate(t *testing.T) { - pb := NewPathBuffer([]byte(""), 0) - res := &ValidateResult{} + pb := huma.NewPathBuffer([]byte(""), 0) + res := &huma.ValidateResult{} for _, test := range validateTests { t.Run(test.name, func(t *testing.T) { - registry := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + registry := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) - var s *Schema + var s *huma.Schema if test.panic != "" { assert.Panics(t, func() { registry.Schema(test.typ, false, "TestInput") @@ -859,11 +868,11 @@ func TestValidate(t *testing.T) { pb.Reset() res.Reset() - Validate(registry, s, pb, test.mode, test.input, res) + huma.Validate(registry, s, pb, test.mode, test.input, res) if len(test.errs) > 0 { errs := mapTo(res.Errors, func(e error) string { - return e.(*ErrorDetail).Message + return e.(*huma.ErrorDetail).Message }) schemaJSON, _ := json.MarshalIndent(registry.Map(), "", " ") for _, err := range test.errs { @@ -891,7 +900,7 @@ func ExampleModelValidator() { json.Unmarshal([]byte(`{"name": "abcdefg", "age": 1}`), &val) // Validate the unmarshaled data against the type and print errors. - validator := NewModelValidator() + validator := huma.NewModelValidator() errs := validator.Validate(typ, val) fmt.Println(errs) @@ -904,12 +913,12 @@ func ExampleModelValidator() { // [] } -var BenchValidatePB *PathBuffer -var BenchValidateRes *ValidateResult +var BenchValidatePB *huma.PathBuffer +var BenchValidateRes *huma.ValidateResult func BenchmarkValidate(b *testing.B) { - pb := NewPathBuffer([]byte(""), 0) - res := &ValidateResult{} + pb := huma.NewPathBuffer([]byte(""), 0) + res := &huma.ValidateResult{} BenchValidatePB = pb BenchValidateRes = res @@ -919,11 +928,11 @@ func BenchmarkValidate(b *testing.B) { } b.Run(strings.TrimSuffix(test.name, " success"), func(b *testing.B) { - registry := NewMapRegistry("#/components/schemas/", DefaultSchemaNamer) + registry := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer) s := registry.Schema(test.typ, false, "TestInput") input := test.input - if s.Type == TypeObject && s.Properties["value"] != nil { + if s.Type == huma.TypeObject && s.Properties["value"] != nil { s = s.Properties["value"] input = input.(map[string]any)["value"] } @@ -933,7 +942,7 @@ func BenchmarkValidate(b *testing.B) { for i := 0; i < b.N; i++ { pb.Reset() res.Reset() - Validate(registry, s, pb, test.mode, input, res) + huma.Validate(registry, s, pb, test.mode, input, res) } }) }