Repository: swaggest/rest
Pull request #14 (2020-12-28 13:41:08)

Add more tests

github-actions[bot]: ### Unit Test Coverage
total:	(statements)	78.5%
<details><summary>Coverage diff with base branch</summary>

```diff
79,82c79,82
< github.com/swaggest/rest/openapi/collector.go	provideParametersJSONSchemas	0.0%
< github.com/swaggest/rest/openapi/collector.go	ProvideRequestJSONSchemas	0.0%
< github.com/swaggest/rest/openapi/collector.go	provideHeaderSchemas	0.0%
< github.com/swaggest/rest/openapi/collector.go	ProvideResponseJSONSchemas	0.0%
---
> github.com/swaggest/rest/openapi/collector.go	provideParametersJSONSchemas	61.1%
> github.com/swaggest/rest/openapi/collector.go	ProvideRequestJSONSchemas	50.0%
> github.com/swaggest/rest/openapi/collector.go	provideHeaderSchemas	11.8%
> github.com/swaggest/rest/openapi/collector.go	ProvideResponseJSONSchemas	57.9%
84,85c84,85
< github.com/swaggest/rest/request.go	Error	0.0%
< github.com/swaggest/rest/request.go	Fields	0.0%
---
> github.com/swaggest/rest/request.go	Error	100.0%
> github.com/swaggest/rest/request.go	Fields	100.0%
167c167
< total:	(statements)	75.2%
---
> total:	(statements)	78.5%
```
</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: # [Codecov](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=h1) Report
> Merging [#14](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=desc) (225dfb4) into [master](https://codecov.io/gh/swaggest/rest/commit/2ad512e9363680be77df5f0098726a8d371427ce?el=desc) (2ad512e) will **increase** coverage by `2.90%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/swaggest/rest/pull/14/graphs/tree.svg?width=650&height=150&src=pr&token=OpLHxxPdmn)](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #14      +/-   ##
==========================================
+ Coverage   68.36%   71.27%   +2.90%     
==========================================
  Files          26       26              
  Lines        1454     1455       +1     
==========================================
+ Hits          994     1037      +43     
+ Misses        339      289      -50     
- Partials      121      129       +8     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `71.27% <0.00%> (+2.90%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [openapi/collector.go](https://codecov.io/gh/swaggest/rest/pull/14/diff?src=pr&el=tree#diff-b3BlbmFwaS9jb2xsZWN0b3IuZ28=) | `59.80% <0.00%> (+18.46%)` | :arrow_up: |
| [request.go](https://codecov.io/gh/swaggest/rest/pull/14/diff?src=pr&el=tree#diff-cmVxdWVzdC5nbw==) | `100.00% <0.00%> (+100.00%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=footer). Last update [2ad512e...225dfb4](https://codecov.io/gh/swaggest/rest/pull/14?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

github-actions[bot]: ### Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
Using vendor
name                                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    1.64µs ± 3%    1.64µs ± 0%   ~     (p=0.700 n=3+3)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                           871ns ± 4%     951ns ± 2%   ~     (p=0.100 n=3+3)
DecoderFunc_Decode-2                      2.58µs ± 8%    2.61µs ± 1%   ~     (p=0.700 n=3+3)
Decoder_Decode_json-2                     31.8µs ± 8%    31.1µs ± 0%   ~     (p=0.700 n=3+3)
Decoder_Decode_queryObject-2              6.77µs ± 8%    6.91µs ± 1%   ~     (p=0.700 n=3+3)
Decoder_Decode_jsonParam-2                2.63µs ± 0%    2.48µs ± 1%   ~     (p=0.100 n=3+3)
DecoderFactory_SetDecoderFunc-2           2.25µs ± 7%    2.16µs ± 1%   ~     (p=0.700 n=3+3)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              13.7µs ± 6%    15.2µs ± 0%   ~     (p=0.100 n=3+3)
Middleware_control-2                      4.56µs ± 8%    4.27µs ± 2%   ~     (p=0.400 n=3+3)

name                                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2    2.50kB ± 0%    2.50kB ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            448B ± 0%      448B ± 0%   ~     (all equal)
DecoderFunc_Decode-2                      1.51kB ± 0%    1.51kB ± 0%   ~     (all equal)
Decoder_Decode_json-2                     12.4kB ± 0%    12.4kB ± 0%   ~     (all equal)
Decoder_Decode_queryObject-2              2.00kB ± 0%    2.00kB ± 0%   ~     (all equal)
Decoder_Decode_jsonParam-2                  752B ± 0%      752B ± 0%   ~     (all equal)
DecoderFactory_SetDecoderFunc-2           1.02kB ± 0%    1.02kB ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                              1.12kB ± 4%    1.14kB ± 2%   ~     (p=0.600 n=3+3)
Middleware_control-2                      11.2kB ± 0%    11.2kB ± 0%   ~     (all equal)

name                                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/jsonschema goos:linux goarch:amd64
RequestValidator_ValidateRequestData-2      8.00 ± 0%      8.00 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/request goos:linux goarch:amd64
Decoder_Decode-2                            4.00 ± 0%      4.00 ± 0%   ~     (all equal)
DecoderFunc_Decode-2                        12.0 ± 0%      12.0 ± 0%   ~     (all equal)
Decoder_Decode_json-2                        169 ± 0%       169 ± 0%   ~     (all equal)
Decoder_Decode_queryObject-2                36.0 ± 0%      36.0 ± 0%   ~     (all equal)
Decoder_Decode_jsonParam-2                  12.0 ± 0%      12.0 ± 0%   ~     (all equal)
DecoderFactory_SetDecoderFunc-2             16.0 ± 0%      16.0 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/response/gzip goos:linux goarch:amd64
Middleware-2                                11.0 ± 0%      11.0 ± 0%   ~     (all equal)
Middleware_control-2                        9.00 ± 0%      9.00 ± 0%   ~     (all equal)
```
</details>

### Examples Benchmark Result
<details><summary>Benchmark diff with base branch</summary>

```
Using vendor
name                    old time/op    new time/op    delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2             39.3µs ± 4%    27.8µs ± 1%   ~     (p=0.100 n=3+3)
_directGzipHead-2         36.1µs ± 5%    27.4µs ± 1%   ~     (p=0.100 n=3+3)
_noDirectGzip-2            171µs ± 6%     149µs ± 1%   ~     (p=0.100 n=3+3)
_directGzip_decode-2       612µs ± 2%     576µs ± 2%   ~     (p=0.100 n=3+3)
_noDirectGzip_decode-2     157µs ± 2%     149µs ± 1%   ~     (p=0.100 n=3+3)
_jsonBody-2               45.7µs ± 3%    33.6µs ± 1%   ~     (p=0.100 n=3+3)
_jsonBodyValidation-2     73.6µs ± 2%    58.5µs ± 3%   ~     (p=0.100 n=3+3)
_outputHeaders-2          36.1µs ± 2%    26.6µs ± 3%   ~     (p=0.100 n=3+3)
_validation-2             62.9µs ± 2%    53.2µs ± 3%   ~     (p=0.100 n=3+3)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2            45.0µs ±10%    33.8µs ± 2%   ~     (p=0.100 n=3+3)
_ok-2                     43.3µs ± 1%    35.1µs ± 1%   ~     (p=0.100 n=3+3)
_invalidBody-2            61.8µs ± 7%    49.0µs ± 2%   ~     (p=0.100 n=3+3)

name                    old B:rcvd/op  new B:rcvd/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                639 ± 0%       639 ± 0%   ~     (all equal)
_directGzipHead-2            183 ± 0%       183 ± 0%   ~     (all equal)
_noDirectGzip-2            1.04k ± 0%     1.04k ± 0%   ~     (all equal)
_directGzip_decode-2         639 ± 0%       639 ± 0%   ~     (all equal)
_noDirectGzip_decode-2     1.04k ± 0%     1.04k ± 0%   ~     (all equal)
_jsonBody-2                  194 ± 0%       194 ± 0%   ~     (all equal)
_jsonBodyValidation-2        194 ± 0%       194 ± 0%   ~     (all equal)
_outputHeaders-2             155 ± 0%       155 ± 0%   ~     (all equal)
_validation-2                177 ± 0%       177 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2               337 ± 0%       337 ± 0%   ~     (all equal)
_ok-2                        359 ± 0%       359 ± 0%   ~     (p=1.000 n=3+3)
_invalidBody-2               435 ± 0%       435 ± 0%   ~     (all equal)

name                    old B:sent/op  new B:sent/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2                103 ± 0%       103 ± 0%   ~     (all equal)
_directGzipHead-2            104 ± 0%       104 ± 0%   ~     (all equal)
_noDirectGzip-2              117 ± 0%       117 ± 0%   ~     (all equal)
_directGzip_decode-2         116 ± 0%       116 ± 0%   ~     (all equal)
_noDirectGzip_decode-2       130 ± 0%       130 ± 0%   ~     (all equal)
_jsonBody-2                  181 ± 0%       181 ± 0%   ~     (all equal)
_jsonBodyValidation-2        192 ± 0%       192 ± 0%   ~     (all equal)
_outputHeaders-2            77.0 ± 0%      77.0 ± 0%   ~     (all equal)
_validation-2                170 ± 0%       170 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2              74.0 ± 0%      74.0 ± 0%   ~     (all equal)
_ok-2                       74.0 ± 0%      74.0 ± 0%   ~     (all equal)
_invalidBody-2               137 ± 0%       137 ± 0%   ~     (all equal)

name                    old rps        new rps        delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2              25.5k ± 4%     35.9k ± 1%   ~     (p=0.100 n=3+3)
_directGzipHead-2          27.7k ± 4%     36.5k ± 1%   ~     (p=0.100 n=3+3)
_noDirectGzip-2            5.86k ± 6%     6.69k ± 1%   ~     (p=0.100 n=3+3)
_directGzip_decode-2       1.63k ± 2%     1.74k ± 2%   ~     (p=0.100 n=3+3)
_noDirectGzip_decode-2     6.37k ± 2%     6.70k ± 1%   ~     (p=0.100 n=3+3)
_jsonBody-2                21.9k ± 3%     29.7k ± 1%   ~     (p=0.100 n=3+3)
_jsonBodyValidation-2      13.6k ± 2%     17.1k ± 3%   ~     (p=0.100 n=3+3)
_outputHeaders-2           27.7k ± 2%     37.6k ± 3%   ~     (p=0.100 n=3+3)
_validation-2              15.9k ± 2%     18.8k ± 3%   ~     (p=0.100 n=3+3)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2             22.3k ± 9%     29.6k ± 2%   ~     (p=0.100 n=3+3)
_ok-2                      23.1k ± 1%     28.5k ± 1%   ~     (p=0.100 n=3+3)
_invalidBody-2             16.2k ± 6%     20.4k ± 2%   ~     (p=0.100 n=3+3)

name                    old alloc/op   new alloc/op   delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2             3.87kB ± 0%    3.86kB ± 0%   ~     (p=0.100 n=3+3)
_directGzipHead-2         3.87kB ± 0%    3.86kB ± 0%   ~     (p=0.100 n=3+3)
_noDirectGzip-2           7.28kB ± 6%    6.78kB ± 5%   ~     (p=0.400 n=3+3)
_directGzip_decode-2       403kB ± 0%     403kB ± 0%   ~     (p=0.100 n=3+3)
_noDirectGzip_decode-2    5.10kB ± 6%    5.12kB ± 4%   ~     (p=1.000 n=3+3)
_jsonBody-2               5.99kB ± 0%    5.99kB ± 0%   ~     (p=0.100 n=3+3)
_jsonBodyValidation-2     18.2kB ± 0%    18.2kB ± 0%   ~     (p=0.100 n=3+3)
_outputHeaders-2          3.64kB ± 0%    3.64kB ± 0%   ~     (p=0.100 n=3+3)
_validation-2             16.2kB ± 0%    16.2kB ± 0%   ~     (p=0.100 n=3+3)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2            5.65kB ± 0%    5.65kB ± 0%   ~     (p=0.300 n=3+3)
_ok-2                     5.63kB ± 3%    5.63kB ± 2%   ~     (p=0.800 n=3+3)
_invalidBody-2            9.64kB ± 3%    9.62kB ± 3%   ~     (p=0.400 n=3+3)

name                    old allocs/op  new allocs/op  delta
pkg:github.com/swaggest/rest/_examples/advanced goos:linux goarch:amd64
_directGzip-2               40.0 ± 0%      40.0 ± 0%   ~     (all equal)
_directGzipHead-2           40.0 ± 0%      40.0 ± 0%   ~     (all equal)
_noDirectGzip-2             49.0 ± 0%      49.0 ± 0%   ~     (all equal)
_directGzip_decode-2         498 ± 0%       498 ± 0%   ~     (all equal)
_noDirectGzip_decode-2      48.0 ± 0%      48.0 ± 0%   ~     (all equal)
_jsonBody-2                 57.0 ± 0%      57.0 ± 0%   ~     (all equal)
_jsonBodyValidation-2        144 ± 0%       144 ± 0%   ~     (all equal)
_outputHeaders-2            35.0 ± 0%      35.0 ± 0%   ~     (all equal)
_validation-2                131 ± 0%       131 ± 0%   ~     (all equal)
pkg:github.com/swaggest/rest/_examples/task-api/internal/infra/nethttp goos:linux goarch:amd64
_notFoundSrv-2              63.0 ± 0%      63.0 ± 0%   ~     (all equal)
_ok-2                       60.0 ± 3%      59.7 ± 2%   ~     (p=1.000 n=3+3)
_invalidBody-2               110 ± 2%       110 ± 2%   ~     (p=1.000 n=3+3)
```
</details>
<!-- Sticky Pull Request Commentbench -->

Patch:
diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml
index f16cc71..417d94c 100644
--- a/.github/workflows/bench.yml
+++ b/.github/workflows/bench.yml
@@ -55,7 +55,7 @@ jobs:
         id: bench
         run: |
           export REF_NAME=${GITHUB_REF##*/}
-          BENCH_COUNT=3 make bench-run bench-stat bench-run-examples bench-stat-examples
+          BENCH_COUNT=5 make bench-run bench-stat bench-run-examples bench-stat-examples
           OUTPUT=$(make bench-stat)
           OUTPUT="${OUTPUT//'%'/'%25'}"
           OUTPUT="${OUTPUT//$'\n'/'%0A'}"
diff --git a/jsonschema/validator.go b/jsonschema/validator.go
index fac3fcf..ecb5cae 100644
--- a/jsonschema/validator.go
+++ b/jsonschema/validator.go
@@ -61,6 +61,8 @@ func (f Factory) MakeRequestValidator(
 }
 
 // MakeResponseValidator creates response validator.
+//
+// Header mapping is a map of struct field name to header name.
 func (f Factory) MakeResponseValidator(
 	statusCode int,
 	contentType string,
diff --git a/openapi/collector.go b/openapi/collector.go
index d3dc6e7..360918c 100644
--- a/openapi/collector.go
+++ b/openapi/collector.go
@@ -252,9 +252,11 @@ func (c *Collector) provideParametersJSONSchemas(op openapi3.Operation, validato
 			required = true
 		}
 
-		err = validator.AddSchema(rest.ParamIn(pp.In), pp.Name, schemaData, required)
-		if err != nil {
-			return fmt.Errorf("failed to add validation schema for parameter (%s, %s): %w", pp.In, pp.Name, err)
+		if validator != nil {
+			err = validator.AddSchema(rest.ParamIn(pp.In), pp.Name, schemaData, required)
+			if err != nil {
+				return fmt.Errorf("failed to add validation schema for parameter (%s, %s): %w", pp.In, pp.Name, err)
+			}
 		}
 	}
 
@@ -346,9 +348,11 @@ func (c *Collector) provideHeaderSchemas(resp *openapi3.Response, validator rest
 			required = true
 		}
 
-		err = validator.AddSchema(rest.ParamInHeader, name, schemaData, required)
-		if err != nil {
-			return fmt.Errorf("failed to add validation schema for response header (%s): %w", name, err)
+		if validator != nil {
+			err = validator.AddSchema(rest.ParamInHeader, name, schemaData, required)
+			if err != nil {
+				return fmt.Errorf("failed to add validation schema for response header (%s): %w", name, err)
+			}
 		}
 	}
 
diff --git a/trait.go b/trait.go
index 0200ade..8b8ca07 100644
--- a/trait.go
+++ b/trait.go
@@ -54,12 +54,14 @@ func OutputHasNoContent(output interface{}) bool {
 	_, withWriter := output.(usecase.OutputWithWriter)
 	_, noContent := output.(usecase.OutputWithNoContent)
 
+	rv := reflect.ValueOf(output)
+
 	if !withWriter && !noContent &&
 		!refl.HasTaggedFields(output, "json") &&
 		!refl.IsSliceOrMap(output) &&
 		refl.FindEmbeddedSliceOrMap(output) == nil &&
 		!refl.As(output, new(json.Marshaler)) &&
-		reflect.ValueOf(output).Elem().Kind() != reflect.Interface {
+		(rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Interface) {
 		return true
 	}
 


Test patch:
diff --git a/jsonschema/validator_test.go b/jsonschema/validator_test.go
index 05087a2..dc8ba5a 100644
--- a/jsonschema/validator_test.go
+++ b/jsonschema/validator_test.go
@@ -59,3 +59,22 @@ func TestRequestValidator_ValidateData(t *testing.T) {
 	assert.NoError(t, validator.ValidateData("unknown", map[string]interface{}{}))
 	assert.NoError(t, validator.ValidateData(rest.ParamInCookie, map[string]interface{}{"in_cookie": "abc"}))
 }
+
+func TestFactory_MakeResponseValidator(t *testing.T) {
+	validator := jsonschema.NewFactory(&openapi.Collector{}, &openapi.Collector{}).
+		MakeResponseValidator(http.StatusOK, "application/json", new(struct {
+			Name  string `json:"name" minLength:"1"`
+			Trace string `maxLength:"3"`
+		}), map[string]string{
+			"Trace": "X-Trace",
+		})
+
+	assert.NoError(t, validator.ValidateJSONBody([]byte(`{"name":"John"}`)))
+	assert.Error(t, validator.ValidateJSONBody([]byte(`{"name":""}`))) // minLength:"1" violated.
+	assert.NoError(t, validator.ValidateData(rest.ParamInHeader, map[string]interface{}{
+		"X-Trace": "abc",
+	}))
+	assert.Error(t, validator.ValidateData(rest.ParamInHeader, map[string]interface{}{
+		"X-Trace": "abcd", // maxLength:"3" violated.
+	}))
+}
diff --git a/openapi/collector_test.go b/openapi/collector_test.go
index c60f68d..869d30b 100644
--- a/openapi/collector_test.go
+++ b/openapi/collector_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/swaggest/assertjson"
 	"github.com/swaggest/openapi-go/openapi3"
@@ -17,6 +18,31 @@ import (
 	"github.com/swaggest/usecase/status"
 )
 
+var _ rest.JSONSchemaValidator = validatorMock{}
+
+type validatorMock struct {
+	ValidateDataFunc     func(in rest.ParamIn, namedData map[string]interface{}) error
+	ValidateJSONBodyFunc func(jsonBody []byte) error
+	HasConstraintsFunc   func(in rest.ParamIn) bool
+	AddSchemaFunc        func(in rest.ParamIn, name string, schemaData []byte, required bool) error
+}
+
+func (v validatorMock) ValidateData(in rest.ParamIn, namedData map[string]interface{}) error {
+	return v.ValidateDataFunc(in, namedData)
+}
+
+func (v validatorMock) ValidateJSONBody(jsonBody []byte) error {
+	return v.ValidateJSONBodyFunc(jsonBody)
+}
+
+func (v validatorMock) HasConstraints(in rest.ParamIn) bool {
+	return v.HasConstraintsFunc(in)
+}
+
+func (v validatorMock) AddSchema(in rest.ParamIn, name string, schemaData []byte, required bool) error {
+	return v.AddSchemaFunc(in, name, schemaData, required)
+}
+
 func TestCollector_Collect(t *testing.T) {
 	c := openapi.Collector{
 		BasePath: "http://example.com/",
@@ -37,14 +63,15 @@ func TestCollector_Collect(t *testing.T) {
 
 	type input struct {
 		Q string  `query:"q" required:"true"`
-		H int     `header:"h"`
+		H int     `header:"h" minimum:"10"`
 		F float32 `formData:"f"`
 		C bool    `cookie:"c"`
 	}
 
 	type output struct {
-		Name   string `json:"name"`
+		Name   string `json:"name" maxLength:"32"`
 		Number int    `json:"number"`
+		Trace  string `header:"X-Trace"`
 	}
 
 	u.SetTitle("Create Task")
@@ -73,11 +100,18 @@ func TestCollector_Collect(t *testing.T) {
 	c.ServeHTTP(rw, nil)
 
 	assertjson.Equal(t, j, rw.Body.Bytes())
+
+	val := validatorMock{
+		AddSchemaFunc: func(in rest.ParamIn, name string, schemaData []byte, required bool) error {
+			return nil
+		},
+	}
+	assert.NoError(t, c.ProvideResponseJSONSchemas(http.StatusOK, "application/json", new(output), nil, val))
 }
 
 func TestCollector_Collect_requestMapping(t *testing.T) {
 	type input struct {
-		InHeader   string
+		InHeader   string `minLength:"2"`
 		InQuery    int
 		InCookie   float64
 		InFormData string
@@ -97,14 +131,16 @@ func TestCollector_Collect_requestMapping(t *testing.T) {
 	u.SetIsDeprecated(true)
 	u.Input = new(input)
 
+	mapping := rest.RequestMapping{
+		rest.ParamInFormData: map[string]string{"InFormData": "in_form_data", "InFile": "upload"},
+		rest.ParamInCookie:   map[string]string{"InCookie": "in_cookie"},
+		rest.ParamInQuery:    map[string]string{"InQuery": "in_query"},
+		rest.ParamInHeader:   map[string]string{"InHeader": "X-In-Header"},
+		rest.ParamInPath:     map[string]string{"InPath": "in-path"},
+	}
+
 	h := rest.HandlerTrait{
-		ReqMapping: rest.RequestMapping{
-			rest.ParamInFormData: map[string]string{"InFormData": "in_form_data", "InFile": "upload"},
-			rest.ParamInCookie:   map[string]string{"InCookie": "in_cookie"},
-			rest.ParamInQuery:    map[string]string{"InQuery": "in_query"},
-			rest.ParamInHeader:   map[string]string{"InHeader": "X-In-Header"},
-			rest.ParamInPath:     map[string]string{"InPath": "in-path"},
-		},
+		ReqMapping: mapping,
 	}
 
 	collector := openapi.Collector{}
@@ -124,7 +160,7 @@ func TestCollector_Collect_requestMapping(t *testing.T) {
 			  {"name":"in_query","in":"query","schema":{"type":"integer"}},
 			  {"name":"in-path","in":"path","required":true,"schema":{"type":"boolean"}},
 			  {"name":"in_cookie","in":"cookie","schema":{"type":"number"}},
-			  {"name":"X-In-Header","in":"header","schema":{"type":"string"}}
+			  {"name":"X-In-Header","in":"header","schema":{"minLength":2,"type":"string"}}
 			],
 			"requestBody":{
 			  "content":{
@@ -148,4 +184,11 @@ func TestCollector_Collect_requestMapping(t *testing.T) {
 		}
 	  }
 	}`), j, string(j))
+
+	val := validatorMock{
+		AddSchemaFunc: func(in rest.ParamIn, name string, schemaData []byte, required bool) error {
+			return nil
+		},
+	}
+	assert.NoError(t, collector.ProvideRequestJSONSchemas(http.MethodPost, new(input), mapping, val))
 }
diff --git a/request_test.go b/request_test.go
new file mode 100644
index 0000000..bc7e9e8
--- /dev/null
+++ b/request_test.go
@@ -0,0 +1,17 @@
+package rest_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/swaggest/rest"
+)
+
+func TestRequestErrors_Error(t *testing.T) {
+	err := rest.RequestErrors{
+		"foo": []string{"bar"},
+	}
+
+	assert.EqualError(t, err, "bad request")
+	assert.Equal(t, map[string]interface{}{"foo": []string{"bar"}}, err.Fields())
+}
diff --git a/trait_test.go b/trait_test.go
new file mode 100644
index 0000000..1ea426a
--- /dev/null
+++ b/trait_test.go
@@ -0,0 +1,21 @@
+package rest_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/swaggest/rest"
+)
+
+func TestHandlerTrait_RestHandler(t *testing.T) {
+	h := &rest.HandlerTrait{
+		ReqMapping: map[rest.ParamIn]map[string]string{},
+	}
+	assert.Equal(t, h, h.RestHandler())
+	assert.Equal(t, h.ReqMapping, h.RequestMapping())
+}
+
+func TestOutputHasNoContent(t *testing.T) {
+	assert.True(t, rest.OutputHasNoContent(nil))
+	assert.True(t, rest.OutputHasNoContent(struct{}{}))
+}