Repository: danielgtaylor/huma
Pull request #122 (2023-09-06 23:49:10)

fix: improved tests, fixed a few small issues
Improved tests & coverage for v2. Fixed a few issues:

- Incorrect `time.Time` handling in some cases, or unexpected validation failures
- Better validation of pre-parsed params (e.g. `int` vs the JSON/CBOR `float64`, `[]string` fixes)
- Default content type assumption for request bodies if missing `Content-Type` header
- `Status304NotModied` → `Status304NotModified` 🤭 



Patch:
diff --git a/api.go b/api.go
index 01bb51b..a940489 100644
--- a/api.go
+++ b/api.go
@@ -158,7 +158,12 @@ func (r *api) Unmarshal(contentType string, data []byte, v any) error {
 	if end == -1 {
 		end = len(contentType)
 	}
-	f, ok := r.formats[contentType[start:end]]
+	ct := contentType[start:end]
+	if ct == "" {
+		// Default to assume JSON since this is an API.
+		ct = "application/json"
+	}
+	f, ok := r.formats[ct]
 	if !ok {
 		return fmt.Errorf("unknown content type: %s", contentType)
 	}
@@ -177,7 +182,6 @@ func (r *api) Negotiate(accept string) (string, error) {
 }
 
 func (a *api) Marshal(ctx Context, respKey string, ct string, v any) error {
-	// fmt.Println("marshaling", ct)
 	var err error
 
 	for _, t := range a.transformers {
diff --git a/conditional/params.go b/conditional/params.go
index 1d35463..06a3842 100644
--- a/conditional/params.go
+++ b/conditional/params.go
@@ -134,7 +134,7 @@ func (p *Params) PreconditionFailed(etag string, modified time.Time) huma.Status
 			)
 		}
 
-		return huma.Status304NotModied()
+		return huma.Status304NotModified()
 	}
 
 	return nil
diff --git a/error.go b/error.go
index 7a92dbf..49ddd8f 100644
--- a/error.go
+++ b/error.go
@@ -108,10 +108,6 @@ type StatusError interface {
 	Error() string
 }
 
-// Ensure the default error model satisfies these interfaces.
-var _ StatusError = (*ErrorModel)(nil)
-var _ ContentTypeFilter = (*ErrorModel)(nil)
-
 // NewError creates a new instance of an error model with the given status code,
 // message, and errors. If the error implements the `ErrorDetailer` interface,
 // the error details will be used. Otherwise, the error message will be used.
@@ -149,9 +145,9 @@ func WriteErr(api API, ctx Context, status int, msg string, errs ...error) {
 	api.Marshal(ctx, strconv.Itoa(status), ct, err)
 }
 
-// Status304NotModied returns a 304. This is not really an error, but provides
-// a way to send non-default responses.
-func Status304NotModied() StatusError {
+// Status304NotModified returns a 304. This is not really an error, but
+// provides a way to send non-default responses.
+func Status304NotModified() StatusError {
 	return NewError(http.StatusNotModified, "")
 }
 
diff --git a/huma.go b/huma.go
index 23ad866..1ae579a 100644
--- a/huma.go
+++ b/huma.go
@@ -24,13 +24,12 @@ var bodyCallbackType = reflect.TypeOf(func(Context) {})
 // if possible. If not, it will not incur any allocations (unlike the stdlib
 // `http.ResponseController`).
 func SetReadDeadline(w http.ResponseWriter, deadline time.Time) error {
-	rw := w
 	for {
-		switch t := rw.(type) {
+		switch t := w.(type) {
 		case interface{ SetReadDeadline(time.Time) error }:
 			return t.SetReadDeadline(deadline)
 		case interface{ Unwrap() http.ResponseWriter }:
-			rw = t.Unwrap()
+			w = t.Unwrap()
 		default:
 			return errDeadlineUnsupported
 		}
@@ -170,12 +169,13 @@ type findResult[T comparable] struct {
 }
 
 func (r *findResult[T]) every(current reflect.Value, path []int, v T, f func(reflect.Value, T)) {
+	if len(path) == 0 {
+		f(current, v)
+		return
+	}
+
 	switch current.Kind() {
 	case reflect.Struct:
-		if len(path) == 0 {
-			f(current, v)
-			return
-		}
 		r.every(reflect.Indirect(current.Field(path[0])), path[1:], v, f)
 	case reflect.Slice:
 		for j := 0; j < current.Len(); j++ {
@@ -186,10 +186,6 @@ func (r *findResult[T]) every(current reflect.Value, path []int, v T, f func(ref
 			r.every(reflect.Indirect(current.MapIndex(k)), path, v, f)
 		}
 	default:
-		if len(path) == 0 {
-			f(current, v)
-			return
-		}
 		panic("unsupported")
 	}
 }
@@ -619,6 +615,9 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I)
 
 			buf := bufPool.Get().(*bytes.Buffer)
 			reader := ctx.BodyReader()
+			if reader == nil {
+				reader = bytes.NewReader(nil)
+			}
 			if closer, ok := reader.(io.Closer); ok {
 				defer closer.Close()
 			}
diff --git a/schema.go b/schema.go
index 41ad78d..41d0f97 100644
--- a/schema.go
+++ b/schema.go
@@ -257,6 +257,12 @@ func SchemaFromField(registry Registry, parent reflect.Type, f reflect.StructFie
 		return fs
 	}
 	fs.Description = f.Tag.Get("doc")
+	if fs.Format == "date-time" && f.Tag.Get("header") != "" {
+		// Special case: this is a header and uses a different date/time format.
+		// Note that it can still be overridden by the `format` or `timeFormat`
+		// tags later.
+		fs.Format = "date-time-http"
+	}
 	if fmt := f.Tag.Get("format"); fmt != "" {
 		fs.Format = fmt
 	}
diff --git a/validate.go b/validate.go
index 7503bd0..6798fc5 100644
--- a/validate.go
+++ b/validate.go
@@ -185,6 +185,10 @@ func validateFormat(path *PathBuffer, str string, s *Schema, res *ValidateResult
 		if !found {
 			res.Add(path, str, "expected string to be RFC 3339 date-time")
 		}
+	case "date-time-http":
+		if _, err := time.Parse(time.RFC1123, str); err != nil {
+			res.Add(path, str, "expected string to be RFC 1123 date-time")
+		}
 	case "date":
 		if _, err := time.Parse("2006-01-02", str); err != nil {
 			res.Add(path, str, "expected string to be RFC 3339 date")
@@ -271,10 +275,28 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any,
 		switch v := v.(type) {
 		case float64:
 			num = v
+		case float32:
+			num = float64(v)
 		case int:
 			num = float64(v)
+		case int8:
+			num = float64(v)
+		case int16:
+			num = float64(v)
+		case int32:
+			num = float64(v)
 		case int64:
 			num = float64(v)
+		case uint:
+			num = float64(v)
+		case uint8:
+			num = float64(v)
+		case uint16:
+			num = float64(v)
+		case uint32:
+			num = float64(v)
+		case uint64:
+			num = float64(v)
 		default:
 			res.Add(path, v, "expected number")
 			return
@@ -342,38 +364,16 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any,
 			}
 		}
 	case TypeArray:
-		arr, ok := v.([]any)
-		if !ok {
+		switch arr := v.(type) {
+		case []any:
+			handleArray(r, s, path, mode, res, arr)
+		case []string:
+			// Special case for params which are lists.
+			handleArray(r, s, path, mode, res, arr)
+		default:
 			res.Add(path, v, "expected array")
 			return
 		}
-
-		if s.MinItems != nil {
-			if len(arr) < *s.MinItems {
-				res.Addf(path, v, s.msgMinItems)
-			}
-		}
-		if s.MaxItems != nil {
-			if len(arr) > *s.MaxItems {
-				res.Addf(path, v, s.msgMaxItems)
-			}
-		}
-
-		if s.UniqueItems {
-			seen := make(map[any]struct{}, len(arr))
-			for _, item := range arr {
-				if _, ok := seen[item]; ok {
-					res.Add(path, v, "expected array items to be unique")
-				}
-				seen[item] = struct{}{}
-			}
-		}
-
-		for i, item := range arr {
-			path.PushIndex(i)
-			Validate(r, s.Items, path, mode, item, res)
-			path.Pop()
-		}
 	case TypeObject:
 		if vv, ok := v.(map[string]any); ok {
 			handleMapString(r, s, path, mode, vv, res)
@@ -398,6 +398,35 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any,
 	}
 }
 
+func handleArray[T any](r Registry, s *Schema, path *PathBuffer, mode ValidateMode, res *ValidateResult, arr []T) {
+	if s.MinItems != nil {
+		if len(arr) < *s.MinItems {
+			res.Addf(path, arr, s.msgMinItems)
+		}
+	}
+	if s.MaxItems != nil {
+		if len(arr) > *s.MaxItems {
+			res.Addf(path, arr, s.msgMaxItems)
+		}
+	}
+
+	if s.UniqueItems {
+		seen := make(map[any]struct{}, len(arr))
+		for _, item := range arr {
+			if _, ok := seen[item]; ok {
+				res.Add(path, arr, "expected array items to be unique")
+			}
+			seen[item] = struct{}{}
+		}
+	}
+
+	for i, item := range arr {
+		path.PushIndex(i)
+		Validate(r, s.Items, path, mode, item, res)
+		path.Pop()
+	}
+}
+
 func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, m map[string]any, res *ValidateResult) {
 	if s.MinProperties != nil {
 		if len(m) < *s.MinProperties {


Test patch:
diff --git a/api_test.go b/api_test.go
new file mode 100644
index 0000000..145a59e
--- /dev/null
+++ b/api_test.go
@@ -0,0 +1,16 @@
+package huma
+
+import (
+	"testing"
+
+	"github.com/go-chi/chi"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBlankConfig(t *testing.T) {
+	adapter := &testAdapter{chi.NewMux()}
+
+	assert.NotPanics(t, func() {
+		NewAPI(Config{}, adapter)
+	})
+}
diff --git a/error_test.go b/error_test.go
new file mode 100644
index 0000000..3c1b95e
--- /dev/null
+++ b/error_test.go
@@ -0,0 +1,70 @@
+package huma
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// Ensure the default error model satisfies these interfaces.
+var _ StatusError = (*ErrorModel)(nil)
+var _ ContentTypeFilter = (*ErrorModel)(nil)
+
+func TestError(t *testing.T) {
+	err := &ErrorModel{
+		Status: 400,
+		Detail: "test err",
+	}
+
+	// Add some children.
+	err.Add(&ErrorDetail{
+		Message:  "test detail",
+		Location: "body.foo",
+		Value:    "bar",
+	})
+
+	err.Add(fmt.Errorf("plain error"))
+
+	// Confirm errors were added.
+	assert.Equal(t, "test err", err.Error())
+	assert.Len(t, err.Errors, 2)
+	assert.Equal(t, "test detail (body.foo: bar)", err.Errors[0].Error())
+	assert.Equal(t, "plain error", err.Errors[1].Error())
+
+	// Ensure problem content types.
+	assert.Equal(t, "application/problem+json", err.ContentType("application/json"))
+	assert.Equal(t, "application/problem+cbor", err.ContentType("application/cbor"))
+	assert.Equal(t, "other", err.ContentType("other"))
+}
+
+func TestErrorResponses(t *testing.T) {
+	// NotModified has a slightly different signature.
+	assert.Equal(t, 304, Status304NotModified().GetStatus())
+
+	for _, item := range []struct {
+		constructor func(msg string, errs ...error) 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},
+	} {
+		err := item.constructor("test")
+		assert.Equal(t, item.expected, err.GetStatus())
+	}
+}
diff --git a/huma_test.go b/huma_test.go
index 54c40b0..adc7460 100644
--- a/huma_test.go
+++ b/huma_test.go
@@ -129,6 +129,7 @@ func TestFeatures(t *testing.T) {
 		Method   string
 		URL      string
 		Headers  map[string]string
+		Body     string
 		Assert   func(t *testing.T, resp *httptest.ResponseRecorder)
 	}{
 		{
@@ -145,30 +146,207 @@ func TestFeatures(t *testing.T) {
 					QueryDefault float32   `query:"def" default:"135" example:"5"`
 					QueryBefore  time.Time `query:"before"`
 					QueryDate    time.Time `query:"date" timeFormat:"2006-01-02"`
+					QueryUint    uint32    `query:"uint"`
+					QueryBool    bool      `query:"bool"`
+					QueryStrings []string  `query:"strings"`
 					HeaderString string    `header:"String"`
 					HeaderInt    int       `header:"Int"`
+					HeaderDate   time.Time `header:"Date"`
 				}) (*struct{}, error) {
 					assert.Equal(t, "foo", input.PathString)
 					assert.Equal(t, 123, input.PathInt)
 					assert.Equal(t, "bar", input.QueryString)
 					assert.Equal(t, 456, input.QueryInt)
-					assert.Equal(t, float32(135), input.QueryDefault)
+					assert.EqualValues(t, 135, input.QueryDefault)
 					assert.True(t, input.QueryBefore.Equal(time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)))
 					assert.True(t, input.QueryDate.Equal(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)))
+					assert.EqualValues(t, 1, input.QueryUint)
+					assert.Equal(t, true, input.QueryBool)
+					assert.Equal(t, []string{"foo", "bar"}, input.QueryStrings)
 					assert.Equal(t, "baz", input.HeaderString)
 					assert.Equal(t, 789, input.HeaderInt)
 					return nil, nil
 				})
 			},
-			Method:  http.MethodGet,
-			URL:     "/test-params/foo/123?string=bar&int=456&before=2023-01-01T12:00:00Z&date=2023-01-01",
-			Headers: map[string]string{"string": "baz", "int": "789"},
+			Method: http.MethodGet,
+			URL:    "/test-params/foo/123?string=bar&int=456&before=2023-01-01T12:00:00Z&date=2023-01-01&uint=1&bool=true&strings=foo,bar",
+			Headers: map[string]string{
+				"string": "baz",
+				"int":    "789",
+				"date":   "Mon, 01 Jan 2023 12:00:00 GMT",
+			},
+		},
+		{
+			Name: "params-error",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method: http.MethodGet,
+					Path:   "/test-params/{int}",
+				}, func(ctx context.Context, input *struct {
+					PathInt     string    `path:"int"`
+					QueryInt    int       `query:"int"`
+					QueryFloat  float32   `query:"float"`
+					QueryBefore time.Time `query:"before"`
+					QueryDate   time.Time `query:"date" timeFormat:"2006-01-02"`
+					QueryUint   uint32    `query:"uint"`
+					QueryBool   bool      `query:"bool"`
+				}) (*struct{}, error) {
+					return nil, nil
+				})
+			},
+			Method: http.MethodGet,
+			URL:    "/test-params/bad?int=bad&float=bad&before=bad&date=bad&uint=bad&bool=bad",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusUnprocessableEntity, resp.Code)
+				assert.Contains(t, resp.Body.String(), "invalid integer")
+				assert.Contains(t, resp.Body.String(), "invalid float")
+				assert.Contains(t, resp.Body.String(), "invalid date/time")
+				assert.Contains(t, resp.Body.String(), "invalid bool")
+			},
+		},
+		{
+			Name: "request-body",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method: http.MethodPut,
+					Path:   "/body",
+				}, func(ctx context.Context, input *struct {
+					RawBody []byte
+					Body    struct {
+						Name string `json:"name"`
+					}
+				}) (*struct{}, error) {
+					assert.Equal(t, `{"name":"foo"}`, string(input.RawBody))
+					assert.Equal(t, "foo", input.Body.Name)
+					return nil, nil
+				})
+			},
+			Method: http.MethodPut,
+			URL:    "/body",
+			// Headers: map[string]string{"Content-Type": "application/json"},
+			Body: `{"name":"foo"}`,
+		},
+		{
+			Name: "request-body-required",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method: http.MethodPut,
+					Path:   "/body",
+				}, func(ctx context.Context, input *struct {
+					Body struct {
+						Name string `json:"name"`
+					}
+				}) (*struct{}, error) {
+					return nil, nil
+				})
+			},
+			Method: http.MethodPut,
+			URL:    "/body",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusBadRequest, resp.Code)
+			},
+		},
+		{
+			Name: "request-body-too-large",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method:       http.MethodPut,
+					Path:         "/body",
+					MaxBodyBytes: 1,
+				}, func(ctx context.Context, input *struct {
+					Body struct {
+						Name string `json:"name"`
+					}
+				}) (*struct{}, error) {
+					return nil, nil
+				})
+			},
+			Method: http.MethodPut,
+			URL:    "/body",
+			Body:   "foobarbaz",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusRequestEntityTooLarge, resp.Code)
+			},
+		},
+		{
+			Name: "request-body-bad-json",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method: http.MethodPut,
+					Path:   "/body",
+				}, func(ctx context.Context, input *struct {
+					Body struct {
+						Name string `json:"name"`
+					}
+				}) (*struct{}, error) {
+					return nil, nil
+				})
+			},
+			Method: http.MethodPut,
+			URL:    "/body",
+			Body:   "{{{",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusBadRequest, resp.Code)
+			},
+		},
+		{
+			Name: "handler-error",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method: http.MethodGet,
+					Path:   "/error",
+				}, func(ctx context.Context, input *struct{}) (*struct{}, error) {
+					return nil, Error403Forbidden("nope")
+				})
+			},
+			Method: http.MethodGet,
+			URL:    "/error",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusForbidden, resp.Code)
+			},
+		},
+		{
+			Name: "response-headers",
+			Register: func(t *testing.T, api API) {
+				type Resp struct {
+					Str   string    `header:"str"`
+					Int   int       `header:"int"`
+					Uint  uint      `header:"uint"`
+					Float float64   `header:"float"`
+					Bool  bool      `header:"bool"`
+					Date  time.Time `header:"date"`
+				}
+
+				Register(api, Operation{
+					Method: http.MethodGet,
+					Path:   "/response-headers",
+				}, func(ctx context.Context, input *struct{}) (*Resp, error) {
+					resp := &Resp{}
+					resp.Str = "str"
+					resp.Int = 1
+					resp.Uint = 2
+					resp.Float = 3.45
+					resp.Bool = true
+					resp.Date = time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
+					return resp, nil
+				})
+			},
+			Method: http.MethodGet,
+			URL:    "/response-headers",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusNoContent, resp.Code)
+				assert.Equal(t, "str", resp.Header().Get("Str"))
+				assert.Equal(t, "1", resp.Header().Get("Int"))
+				assert.Equal(t, "2", resp.Header().Get("Uint"))
+				assert.Equal(t, "3.45", resp.Header().Get("Float"))
+				assert.Equal(t, "true", resp.Header().Get("Bool"))
+				assert.Equal(t, "Sun, 01 Jan 2023 12:00:00 GMT", resp.Header().Get("Date"))
+			},
 		},
 		{
 			Name: "response",
 			Register: func(t *testing.T, api API) {
 				type Resp struct {
-					Foo  string `header:"foo"`
 					Body struct {
 						Greeting string `json:"greeting"`
 					}
@@ -179,7 +357,6 @@ func TestFeatures(t *testing.T) {
 					Path:   "/response",
 				}, func(ctx context.Context, input *struct{}) (*Resp, error) {
 					resp := &Resp{}
-					resp.Foo = "foo"
 					resp.Body.Greeting = "Hello, world!"
 					return resp, nil
 				})
@@ -188,17 +365,86 @@ func TestFeatures(t *testing.T) {
 			URL:    "/response",
 			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
 				assert.Equal(t, http.StatusOK, resp.Code)
-				assert.Equal(t, "foo", resp.Header().Get("Foo"))
 				assert.JSONEq(t, `{"$schema": "https:///schemas/RespBody.json", "greeting":"Hello, world!"}`, resp.Body.String())
 			},
 		},
+		{
+			Name: "response-raw",
+			Register: func(t *testing.T, api API) {
+				type Resp struct {
+					Body []byte
+				}
+
+				Register(api, Operation{
+					Method: http.MethodGet,
+					Path:   "/response-raw",
+				}, func(ctx context.Context, input *struct{}) (*Resp, error) {
+					return &Resp{Body: []byte("hello")}, nil
+				})
+			},
+			Method: http.MethodGet,
+			URL:    "/response-raw",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusOK, resp.Code)
+				assert.Equal(t, `hello`, resp.Body.String())
+			},
+		},
+		{
+			Name: "response-stream",
+			Register: func(t *testing.T, api API) {
+				Register(api, Operation{
+					Method: http.MethodGet,
+					Path:   "/stream",
+				}, func(ctx context.Context, input *struct{}) (*StreamResponse, error) {
+					return &StreamResponse{
+						Body: func(ctx Context) {
+							writer := ctx.BodyWriter()
+							writer.Write([]byte("hel"))
+							writer.Write([]byte("lo"))
+						},
+					}, nil
+				})
+			},
+			Method: http.MethodGet,
+			URL:    "/stream",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, http.StatusOK, resp.Code)
+				assert.Equal(t, `hello`, resp.Body.String())
+			},
+		},
+		{
+			Name: "dynamic-status",
+			Register: func(t *testing.T, api API) {
+				type Resp struct {
+					Status int
+				}
+
+				Register(api, Operation{
+					Method: http.MethodGet,
+					Path:   "/status",
+				}, func(ctx context.Context, input *struct{}) (*Resp, error) {
+					resp := &Resp{}
+					resp.Status = 256
+					return resp, nil
+				})
+			},
+			Method: http.MethodGet,
+			URL:    "/status",
+			Assert: func(t *testing.T, resp *httptest.ResponseRecorder) {
+				assert.Equal(t, 256, resp.Code)
+			},
+		},
 	} {
 		t.Run(feature.Name, func(t *testing.T) {
 			r := chi.NewRouter()
 			api := NewTestAdapter(r, DefaultConfig("Features Test API", "1.0.0"))
 			feature.Register(t, api)
 
-			req, _ := http.NewRequest(feature.Method, feature.URL, nil)
+			var body io.Reader = nil
+			if feature.Body != "" {
+				body = strings.NewReader(feature.Body)
+			}
+			req, _ := http.NewRequest(feature.Method, feature.URL, body)
 			for k, v := range feature.Headers {
 				req.Header.Set(k, v)
 			}
@@ -206,14 +452,43 @@ func TestFeatures(t *testing.T) {
 			r.ServeHTTP(w, req)
 			b, _ := yaml.Marshal(api.OpenAPI())
 			t.Log(string(b))
-			assert.Less(t, w.Code, 300, w.Body.String())
 			if feature.Assert != nil {
 				feature.Assert(t, w)
+			} else {
+				assert.Less(t, w.Code, 300, w.Body.String())
 			}
 		})
 	}
 }
 
+func TestOpenAPI(t *testing.T) {
+	r := chi.NewRouter()
+	api := NewTestAdapter(r, DefaultConfig("Features Test API", "1.0.0"))
+
+	type Resp struct {
+		Body struct {
+			Greeting string `json:"greeting"`
+		}
+	}
+
+	Register(api, Operation{
+		Method: http.MethodGet,
+		Path:   "/test",
+	}, func(ctx context.Context, input *struct{}) (*Resp, error) {
+		resp := &Resp{}
+		resp.Body.Greeting = "Hello, world"
+		return resp, nil
+	})
+
+	for _, url := range []string{"/openapi.json", "/openapi.yaml", "/docs", "/schemas/Resp.json"} {
+		req, _ := http.NewRequest(http.MethodGet, url, nil)
+		w := httptest.NewRecorder()
+		r.ServeHTTP(w, req)
+
+		assert.Equal(t, w.Code, 200, w.Body.String())
+	}
+}
+
 type ExhaustiveErrorsInputBody struct {
 	Name  string `json:"name" maxLength:"10"`
 	Count int    `json:"count" minimum:"1"`
diff --git a/validate_test.go b/validate_test.go
index f9ad670..0517020 100644
--- a/validate_test.go
+++ b/validate_test.go
@@ -35,10 +35,65 @@ var validateTests = []struct {
 		input: 0,
 	},
 	{
-		name:  "float64 success",
+		name:  "int from float64 success",
 		typ:   reflect.TypeOf(0),
 		input: float64(0),
 	},
+	{
+		name:  "int from int8 success",
+		typ:   reflect.TypeOf(0),
+		input: int8(0),
+	},
+	{
+		name:  "int from int16 success",
+		typ:   reflect.TypeOf(0),
+		input: int16(0),
+	},
+	{
+		name:  "int from int32 success",
+		typ:   reflect.TypeOf(0),
+		input: int32(0),
+	},
+	{
+		name:  "int from int64 success",
+		typ:   reflect.TypeOf(0),
+		input: int64(0),
+	},
+	{
+		name:  "int from uint success",
+		typ:   reflect.TypeOf(0),
+		input: uint(0),
+	},
+	{
+		name:  "int from uint8 success",
+		typ:   reflect.TypeOf(0),
+		input: uint8(0),
+	},
+	{
+		name:  "int from uint16 success",
+		typ:   reflect.TypeOf(0),
+		input: uint16(0),
+	},
+	{
+		name:  "int from uint32 success",
+		typ:   reflect.TypeOf(0),
+		input: uint32(0),
+	},
+	{
+		name:  "int from uint64 success",
+		typ:   reflect.TypeOf(0),
+		input: uint64(0),
+	},
+	{
+		name:  "float64 from int success",
+		typ:   reflect.TypeOf(0.0),
+		input: 0,
+	},
+	{
+		name:  "float64 from float32 success",
+		typ:   reflect.TypeOf(0.0),
+		input: float32(0),
+	},
 	{
 		name:  "int64 success",
 		typ:   reflect.TypeOf(0),
@@ -217,6 +272,21 @@ var validateTests = []struct {
 		input: map[string]any{"value": "bad"},
 		errs:  []string{"expected string to be RFC 3339 date-time"},
 	},
+	{
+		name: "date-time-http success",
+		typ: reflect.TypeOf(struct {
+			Value string `json:"value" format:"date-time-http"`
+		}{}),
+		input: map[string]any{"value": []byte("Mon, 01 Jan 2023 12:00:00 GMT")},
+	},
+	{
+		name: "expected date-time-http",
+		typ: reflect.TypeOf(struct {
+			Value time.Time `json:"value" format:"date-time-http"`
+		}{}),
+		input: map[string]any{"value": "bad"},
+		errs:  []string{"expected string to be RFC 1123 date-time"},
+	},
 	{
 		name: "date success",
 		typ: reflect.TypeOf(struct {