Skip to content

Commit

Permalink
Allow custom JSON decoder for request body (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Nov 22, 2020
1 parent 03f14d1 commit 87711ad
Show file tree
Hide file tree
Showing 12 changed files with 97 additions and 51 deletions.
1 change: 0 additions & 1 deletion .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ on:
pull_request:
env:
GO111MODULE: "on"

jobs:
bench:
strategy:
Expand Down
1 change: 1 addition & 0 deletions _examples/advanced/json_body_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

// Benchmark_jsonBody-4 29671 37417 ns/op 194 B:rcvd/op 181 B:sent/op 26705 rps 6068 B/op 58 allocs/op.
// Benchmark_jsonBody-4 29749 35934 ns/op 194 B:rcvd/op 181 B:sent/op 27829 rps 6063 B/op 57 allocs/op.
func Benchmark_jsonBody(b *testing.B) {
r := NewRouter()

Expand Down
8 changes: 4 additions & 4 deletions _examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ replace github.com/swaggest/rest => ../

require (
github.com/andybalholm/brotli v1.0.1 // indirect
github.com/bool64/dev v0.1.6
github.com/bool64/dev v0.1.7
github.com/bool64/httptestbench v0.1.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/kelseyhightower/envconfig v1.4.0
github.com/klauspost/compress v1.11.1 // indirect
github.com/stretchr/testify v1.6.1
github.com/swaggest/assertjson v1.3.0
github.com/swaggest/jsonschema-go v0.3.11
github.com/swaggest/openapi-go v0.2.3
github.com/swaggest/assertjson v1.4.0
github.com/swaggest/jsonschema-go v0.3.12
github.com/swaggest/openapi-go v0.2.4
github.com/swaggest/rest v0.0.0-20201021182948-25d31f7f38d4
github.com/swaggest/swgui v1.0.9
github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8
Expand Down
8 changes: 8 additions & 0 deletions _examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ github.com/bool64/dev v0.1.3/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbsc
github.com/bool64/dev v0.1.5/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/dev v0.1.6 h1:QP8buknknKrPVizPbOKeyCt6/2+iZ309wxjreckowVY=
github.com/bool64/dev v0.1.6/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/dev v0.1.7 h1:1xs4ff6lUFIt7euQCmfjU0Xi7IiSmwcQyMokKdq6qAM=
github.com/bool64/dev v0.1.7/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/httptestbench v0.1.0 h1:Nr+9KjZbrlgmd8ABGfFvEYEnMdbAWxj13YHeQR6OOzA=
github.com/bool64/httptestbench v0.1.0/go.mod h1:xPlusBpnquKisPwqjpgK8fBPwa0q1r+ecFoWxRtNjgU=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
Expand Down Expand Up @@ -75,18 +77,24 @@ github.com/swaggest/assertjson v1.0.0/go.mod h1:mE5ltBbrB+Ya8Xar5OMITxya76vwLZMo
github.com/swaggest/assertjson v1.2.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
github.com/swaggest/assertjson v1.3.0 h1:751J+Plmg68cDGeMwMIq5nEvBw8g+ErG4UMcv9kpHmk=
github.com/swaggest/assertjson v1.3.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
github.com/swaggest/assertjson v1.4.0 h1:WPHYWa102jB8BnO76+86IquNH5e3wllC60TsUT4Iq4U=
github.com/swaggest/assertjson v1.4.0/go.mod h1:mJG4OUkBTu+H4jT2oSQ1Y1DrRSu81PtSJ6bTR6e8avg=
github.com/swaggest/form v3.6.4+incompatible h1:K0EaZjf8C7cScuZgnJG+Sg0GUGAtjPZivTv1jN+1ni4=
github.com/swaggest/form v3.6.4+incompatible/go.mod h1:eTJPpSayjnJlOQQbDYsSHlbREbSAtudNW/Y6HouVWr0=
github.com/swaggest/jsonschema-go v0.2.1/go.mod h1:QFauBdPTrU1UltwocM5FzOWnVjVVtcWkJWG3NlK9sV0=
github.com/swaggest/jsonschema-go v0.2.4/go.mod h1:m4VV88Gbi7lCrt9ckJzigK1rMlEeFjdZUkJr1o5MnDE=
github.com/swaggest/jsonschema-go v0.3.8/go.mod h1:AQijowS82ZcUId9RStqvyGTxyhzYUiVDxelwQxEdOy8=
github.com/swaggest/jsonschema-go v0.3.11 h1:rbYBJhAV2Uz1PB4iJyKbvzGO+o1FsdX60aLi+o67IHU=
github.com/swaggest/jsonschema-go v0.3.11/go.mod h1:lJUbAc2E3OpGrrUGvayONeRBXsFJeedWTpm23jhOtKs=
github.com/swaggest/jsonschema-go v0.3.12 h1:KgzIVv3OYCcwjUBdkYLYcQXQ/0q+40cSwZBtHDF+gKY=
github.com/swaggest/jsonschema-go v0.3.12/go.mod h1:UXDViNdMa0fEpc/QgPu9ac1itOuxgbZyExZI6pg024A=
github.com/swaggest/openapi-go v0.1.3/go.mod h1:Zx4ZgJ7XvlFH9wCOHE7u8RAjLfiHAnCHeaD5kUDujVM=
github.com/swaggest/openapi-go v0.2.2 h1:ZV6tjpR/W97sIOknkaOAt/U2FpqoEptRZrYiksfNKkY=
github.com/swaggest/openapi-go v0.2.2/go.mod h1:GyvI08XD7gnTZxFteI2/Rnk6hgmcSufqG8wVBz/yzqo=
github.com/swaggest/openapi-go v0.2.3 h1:DsbY56dzJQzKLjoJKl15WtykoyF1OJrqIzUUH+ovTDg=
github.com/swaggest/openapi-go v0.2.3/go.mod h1:R4cj4xlxiODL8m+x0pKbWXRZYx67UTYOx0xfH0zxm3k=
github.com/swaggest/openapi-go v0.2.4 h1:7OkZxIJ6PGH9n4ULguE87utNxcEQqa4/2KEmCmCf/rI=
github.com/swaggest/openapi-go v0.2.4/go.mod h1:+PRHGYsaroCoINZDx/m+fxlKaRp+Mwu6QlG4VbloKtI=
github.com/swaggest/refl v0.1.0/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
github.com/swaggest/refl v0.1.2/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
github.com/swaggest/refl v0.1.3/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
Expand Down
7 changes: 3 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ module github.com/swaggest/rest
go 1.13

require (
github.com/bool64/dev v0.1.6
github.com/bool64/dev v0.1.7
github.com/cespare/xxhash/v2 v2.1.1
github.com/go-chi/chi v4.1.2+incompatible
github.com/iancoleman/orderedmap v0.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v2 v2.2.0
github.com/stretchr/testify v1.6.1
github.com/swaggest/assertjson v1.3.0
github.com/swaggest/assertjson v1.4.0
github.com/swaggest/form v3.6.4+incompatible
github.com/swaggest/openapi-go v0.2.3
github.com/swaggest/openapi-go v0.2.4
github.com/swaggest/refl v0.1.5
github.com/swaggest/usecase v0.0.0-20200928062416-27f47131b0f8
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
Expand Down
15 changes: 8 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
github.com/bool64/dev v0.1.0/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/dev v0.1.2/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/dev v0.1.5/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/dev v0.1.6 h1:QP8buknknKrPVizPbOKeyCt6/2+iZ309wxjreckowVY=
github.com/bool64/dev v0.1.6/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/bool64/dev v0.1.7 h1:1xs4ff6lUFIt7euQCmfjU0Xi7IiSmwcQyMokKdq6qAM=
github.com/bool64/dev v0.1.7/go.mod h1:pn52JC52uSgpazChx9CeXyG+S3sW2V36HHoLNBbscdg=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -59,17 +60,17 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/swaggest/assertjson v1.0.0/go.mod h1:mE5ltBbrB+Ya8Xar5OMITxya76vwLZMoPG8aXF7sVQc=
github.com/swaggest/assertjson v1.2.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
github.com/swaggest/assertjson v1.3.0 h1:751J+Plmg68cDGeMwMIq5nEvBw8g+ErG4UMcv9kpHmk=
github.com/swaggest/assertjson v1.3.0/go.mod h1:VR1/jzZHtROewu/SBEdFrBOBuNm1FO9LDjvP9fLf6+I=
github.com/swaggest/assertjson v1.4.0 h1:WPHYWa102jB8BnO76+86IquNH5e3wllC60TsUT4Iq4U=
github.com/swaggest/assertjson v1.4.0/go.mod h1:mJG4OUkBTu+H4jT2oSQ1Y1DrRSu81PtSJ6bTR6e8avg=
github.com/swaggest/form v3.6.4+incompatible h1:K0EaZjf8C7cScuZgnJG+Sg0GUGAtjPZivTv1jN+1ni4=
github.com/swaggest/form v3.6.4+incompatible/go.mod h1:eTJPpSayjnJlOQQbDYsSHlbREbSAtudNW/Y6HouVWr0=
github.com/swaggest/jsonschema-go v0.2.1/go.mod h1:QFauBdPTrU1UltwocM5FzOWnVjVVtcWkJWG3NlK9sV0=
github.com/swaggest/jsonschema-go v0.2.4/go.mod h1:m4VV88Gbi7lCrt9ckJzigK1rMlEeFjdZUkJr1o5MnDE=
github.com/swaggest/jsonschema-go v0.3.11 h1:rbYBJhAV2Uz1PB4iJyKbvzGO+o1FsdX60aLi+o67IHU=
github.com/swaggest/jsonschema-go v0.3.11/go.mod h1:lJUbAc2E3OpGrrUGvayONeRBXsFJeedWTpm23jhOtKs=
github.com/swaggest/jsonschema-go v0.3.12 h1:KgzIVv3OYCcwjUBdkYLYcQXQ/0q+40cSwZBtHDF+gKY=
github.com/swaggest/jsonschema-go v0.3.12/go.mod h1:UXDViNdMa0fEpc/QgPu9ac1itOuxgbZyExZI6pg024A=
github.com/swaggest/openapi-go v0.1.3/go.mod h1:Zx4ZgJ7XvlFH9wCOHE7u8RAjLfiHAnCHeaD5kUDujVM=
github.com/swaggest/openapi-go v0.2.3 h1:DsbY56dzJQzKLjoJKl15WtykoyF1OJrqIzUUH+ovTDg=
github.com/swaggest/openapi-go v0.2.3/go.mod h1:R4cj4xlxiODL8m+x0pKbWXRZYx67UTYOx0xfH0zxm3k=
github.com/swaggest/openapi-go v0.2.4 h1:7OkZxIJ6PGH9n4ULguE87utNxcEQqa4/2KEmCmCf/rI=
github.com/swaggest/openapi-go v0.2.4/go.mod h1:+PRHGYsaroCoINZDx/m+fxlKaRp+Mwu6QlG4VbloKtI=
github.com/swaggest/refl v0.1.0/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
github.com/swaggest/refl v0.1.2/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
github.com/swaggest/refl v0.1.3/go.mod h1:kmYWhxNEvjfRDdMRqpaR/vLULk/SotJs9HFUCIVMK8o=
Expand Down
5 changes: 5 additions & 0 deletions jsonschema/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ func (v *Validator) ValidateJSONBody(jsonBody []byte) error {
return errs
}

// HasConstraints indicates if there are validation rules for parameter location.
func (v *Validator) HasConstraints(in rest.ParamIn) bool {
return len(v.inNamedSchemas[in]) > 0
}

// ValidateData performs validation of a mapped request data.
func (v *Validator) ValidateData(in rest.ParamIn, namedData map[string]interface{}) error {
var errs rest.ValidationErrors
Expand Down
8 changes: 4 additions & 4 deletions openapi/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ func (c *Collector) provideParametersJSONSchemas(op openapi3.Operation, validato
schemaData []byte
)

if !schema.IsTrivial() {
if !schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
schemaData, err = schema.JSONSchemaBytes()
if err != nil {
return fmt.Errorf("failed to build JSON Schema for parameter (%s, %s)", pp.In, pp.Name)
Expand Down Expand Up @@ -301,7 +301,7 @@ func (c *Collector) ProvideRequestJSONSchemas(
}

schema := content.Schema.ToJSONSchema(c.Reflector().Spec)
if schema.IsTrivial() {
if schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
continue
}

Expand Down Expand Up @@ -334,7 +334,7 @@ func (c *Collector) provideHeaderSchemas(resp *openapi3.Response, validator rest
schemaData []byte
)

if !schema.IsTrivial() {
if !schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
schemaData, err = schema.JSONSchemaBytes()
if err != nil {
return fmt.Errorf("failed to build JSON Schema for response header (%s)", name)
Expand Down Expand Up @@ -389,7 +389,7 @@ func (c *Collector) ProvideResponseJSONSchemas(

schema := cont.Schema.ToJSONSchema(c.Reflector().Spec)

if schema.IsTrivial() {
if schema.IsTrivial(c.Reflector().ResolveJSONSchemaRef) {
continue
}

Expand Down
12 changes: 11 additions & 1 deletion request/factory.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package request

import (
"io"
"net/http"
"net/url"
"reflect"
Expand Down Expand Up @@ -28,6 +29,10 @@ const (
type DecoderFactory struct {
ApplyDefaults bool

// JSONReader allows custom JSON decoder for request body.
// If not set encoding/json.Decoder is used.
JSONReader func(rd io.Reader, v interface{}) error

formDecoders map[rest.ParamIn]*form.Decoder
decoderFunctions map[rest.ParamIn]decoderFunc
defaultValDecoder *form.Decoder
Expand Down Expand Up @@ -120,7 +125,12 @@ func (df *DecoderFactory) MakeDecoder(

// Checking for body tags.
if refl.HasTaggedFields(input, jsonTag) || refl.FindEmbeddedSliceOrMap(input) != nil {
m.decoders = append(m.decoders, decodeJSONBody)
if df.JSONReader != nil {
m.decoders = append(m.decoders, decodeJSONBody(df.JSONReader))
} else {
m.decoders = append(m.decoders, decodeJSONBody(readJSON))
}

m.in = append(m.in, rest.ParamInBody)
}

Expand Down
61 changes: 38 additions & 23 deletions request/jsonbody.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,52 @@ var bufPool = sync.Pool{
},
}

func decodeJSONBody(r *http.Request, input interface{}, validator rest.Validator) error {
if r.ContentLength == 0 {
return ErrMissingRequestBody
}
func readJSON(rd io.Reader, v interface{}) error {
d := json.NewDecoder(rd)

return d.Decode(v)
}

contentType := r.Header.Get("Content-Type")
if contentType != "" {
if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
return fmt.Errorf("%w, received: %s", ErrJSONExpected, contentType)
func decodeJSONBody(readJSON func(rd io.Reader, v interface{}) error) valueDecoderFunc {
return func(r *http.Request, input interface{}, validator rest.Validator) error {
if r.ContentLength == 0 {
return ErrMissingRequestBody
}
}

b := bufPool.Get().(*bytes.Buffer) // nolint:errcheck // bufPool is configured to provide *bytes.Buffer.
defer bufPool.Put(b)
contentType := r.Header.Get("Content-Type")
if contentType != "" {
if len(contentType) < 16 || contentType[0:16] != "application/json" { // allow 'application/json;charset=UTF-8'
return fmt.Errorf("%w, received: %s", ErrJSONExpected, contentType)
}
}

b.Reset()
var (
rd io.Reader = r.Body
b *bytes.Buffer
)

rd := io.TeeReader(r.Body, b)
d := json.NewDecoder(rd)
validate := validator != nil && validator.HasConstraints(rest.ParamInBody)

err := d.Decode(&input)
if err != nil {
return fmt.Errorf("failed to decode json: %w", err)
}
if validate {
b = bufPool.Get().(*bytes.Buffer) // nolint:errcheck // bufPool is configured to provide *bytes.Buffer.
defer bufPool.Put(b)

b.Reset()
rd = io.TeeReader(r.Body, b)
}

if validator != nil {
err = validator.ValidateJSONBody(b.Bytes())
err := readJSON(rd, &input)
if err != nil {
return err
return fmt.Errorf("failed to decode json: %w", err)
}
}

return nil
if validator != nil && validate {
err = validator.ValidateJSONBody(b.Bytes())
if err != nil {
return err
}
}

return nil
}
}
14 changes: 7 additions & 7 deletions request/jsonbody_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func Test_decodeJSONBody(t *testing.T) {
}

i := Input{}
assert.NoError(t, decodeJSONBody(createReq, &i, nil))
assert.NoError(t, decodeJSONBody(readJSON)(createReq, &i, nil))
assert.Equal(t, 123, i.Amount)
assert.Equal(t, "248df4b7-aa70-47b8-a036-33ac447e668d", i.CustomerID)
assert.Equal(t, "withdraw", i.Type)
Expand All @@ -37,7 +37,7 @@ func Test_decodeJSONBody(t *testing.T) {
i = Input{}
_, err = createBody.Seek(0, io.SeekStart)
assert.NoError(t, err)
assert.NoError(t, decodeJSONBody(createReq, &i, vl))
assert.NoError(t, decodeJSONBody(readJSON)(createReq, &i, vl))
assert.Equal(t, 123, i.Amount)
assert.Equal(t, "248df4b7-aa70-47b8-a036-33ac447e668d", i.CustomerID)
assert.Equal(t, "withdraw", i.Type)
Expand All @@ -49,7 +49,7 @@ func Test_decodeJSONBody_emptyBody(t *testing.T) {

var i []int

err = decodeJSONBody(req, &i, nil)
err = decodeJSONBody(readJSON)(req, &i, nil)
assert.EqualError(t, err, "missing request body")
}

Expand All @@ -60,7 +60,7 @@ func Test_decodeJSONBody_badContentType(t *testing.T) {

var i []int

err = decodeJSONBody(req, &i, nil)
err = decodeJSONBody(readJSON)(req, &i, nil)
assert.EqualError(t, err, "request with application/json content type expected, received: text/plain")
}

Expand All @@ -70,7 +70,7 @@ func Test_decodeJSONBody_decodeFailed(t *testing.T) {

var i []int

err = decodeJSONBody(req, &i, nil)
err = decodeJSONBody(readJSON)(req, &i, nil)
assert.Error(t, err)
}

Expand All @@ -80,7 +80,7 @@ func Test_decodeJSONBody_unmarshalFailed(t *testing.T) {

var i []int

err = decodeJSONBody(req, &i, nil)
err = decodeJSONBody(readJSON)(req, &i, nil)
assert.EqualError(t, err, "failed to decode json: json: cannot unmarshal number into Go value of type []int")
}

Expand All @@ -94,6 +94,6 @@ func Test_decodeJSONBody_validateFailed(t *testing.T) {
return errors.New("failed")
})

err = decodeJSONBody(req, &i, vl)
err = decodeJSONBody(readJSON)(req, &i, vl)
assert.EqualError(t, err, "failed")
}
8 changes: 8 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ type Validator interface {

// ValidateData validates JSON encoded body and returns error in case of invalid data.
ValidateJSONBody(jsonBody []byte) error

// HasConstraints indicates if there are validation rules for parameter location.
HasConstraints(in ParamIn) bool
}

// ValidatorFunc implements Validator with a func.
Expand All @@ -19,6 +22,11 @@ func (v ValidatorFunc) ValidateData(in ParamIn, namedData map[string]interface{}
return v(in, namedData)
}

// HasConstraints indicates if there are validation rules for parameter location.
func (v ValidatorFunc) HasConstraints(_ ParamIn) bool {
return true
}

// ValidateJSONBody implements Validator.
func (v ValidatorFunc) ValidateJSONBody(body []byte) error {
return v(ParamInBody, map[string]interface{}{"body": json.RawMessage(body)})
Expand Down

0 comments on commit 87711ad

Please sign in to comment.