Skip to content

Commit a631dc6

Browse files
authored
Infer default values from prepared schema (#195)
1 parent af28bea commit a631dc6

File tree

7 files changed

+102
-13
lines changed

7 files changed

+102
-13
lines changed

_examples/go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ require (
1717
github.com/rs/cors v1.10.1
1818
github.com/stretchr/testify v1.9.0
1919
github.com/swaggest/assertjson v1.9.0
20-
github.com/swaggest/jsonschema-go v0.3.66
21-
github.com/swaggest/openapi-go v0.2.47
20+
github.com/swaggest/jsonschema-go v0.3.69
21+
github.com/swaggest/openapi-go v0.2.49
2222
github.com/swaggest/rest v0.0.0-00010101000000-000000000000
2323
github.com/swaggest/swgui v1.8.0
2424
github.com/swaggest/usecase v1.3.1

_examples/go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,10 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
123123
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
124124
github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
125125
github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
126-
github.com/swaggest/jsonschema-go v0.3.66 h1:4c5d7NRRqPLTswsbaypKqcMe3Z+CYHE3/lGsPIByp8o=
127-
github.com/swaggest/jsonschema-go v0.3.66/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
128-
github.com/swaggest/openapi-go v0.2.47 h1:qBh28FHz0M1QSJmGRCcY/Xt9WKRkECKXGUbw/U8IcJ4=
129-
github.com/swaggest/openapi-go v0.2.47/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
126+
github.com/swaggest/jsonschema-go v0.3.69 h1:BNEajhoQjnEQzxZqPmjD1Pcs1pxnjx/X9L5KWllLHxo=
127+
github.com/swaggest/jsonschema-go v0.3.69/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
128+
github.com/swaggest/openapi-go v0.2.49 h1:WxAfdde6DlfQPQayHWTb64CywK9+vpToi7iN17iDPO8=
129+
github.com/swaggest/openapi-go v0.2.49/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
130130
github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
131131
github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
132132
github.com/swaggest/swgui v1.8.0 h1:dPu8TsYIOraaObAkyNdoiLI8mu7nOqQ6SU7HOv254rM=

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ require (
1313
github.com/stretchr/testify v1.8.2
1414
github.com/swaggest/assertjson v1.9.0
1515
github.com/swaggest/form/v5 v5.1.1
16-
github.com/swaggest/jsonschema-go v0.3.66
17-
github.com/swaggest/openapi-go v0.2.47
16+
github.com/swaggest/jsonschema-go v0.3.69
17+
github.com/swaggest/openapi-go v0.2.49
1818
github.com/swaggest/refl v1.3.0
1919
github.com/swaggest/usecase v1.3.1
2020
)

go.sum

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,11 @@ github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7
7878
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
7979
github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY=
8080
github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg=
81-
github.com/swaggest/jsonschema-go v0.3.66 h1:4c5d7NRRqPLTswsbaypKqcMe3Z+CYHE3/lGsPIByp8o=
8281
github.com/swaggest/jsonschema-go v0.3.66/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
83-
github.com/swaggest/openapi-go v0.2.47 h1:qBh28FHz0M1QSJmGRCcY/Xt9WKRkECKXGUbw/U8IcJ4=
84-
github.com/swaggest/openapi-go v0.2.47/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
82+
github.com/swaggest/jsonschema-go v0.3.69 h1:BNEajhoQjnEQzxZqPmjD1Pcs1pxnjx/X9L5KWllLHxo=
83+
github.com/swaggest/jsonschema-go v0.3.69/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE=
84+
github.com/swaggest/openapi-go v0.2.49 h1:WxAfdde6DlfQPQayHWTb64CywK9+vpToi7iN17iDPO8=
85+
github.com/swaggest/openapi-go v0.2.49/go.mod h1:MK5O26lG289kFgMOyXK1VXDoTZ89KJ8Vt0v0ic23zZw=
8586
github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
8687
github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
8788
github.com/swaggest/usecase v1.3.1 h1:JdKV30MTSsDxAXxkldLNcEn8O2uf565khyo6gr5sS+w=

request/factory.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
"github.com/swaggest/form/v5"
14+
"github.com/swaggest/jsonschema-go"
1415
"github.com/swaggest/openapi-go"
1516
"github.com/swaggest/refl"
1617
"github.com/swaggest/rest"
@@ -38,6 +39,9 @@ type DecoderFactory struct {
3839
// If not set encoding/json.Decoder is used.
3940
JSONReader func(rd io.Reader, v interface{}) error
4041

42+
// JSONSchemaReflector is optional, it is called to infer "default" values.
43+
JSONSchemaReflector *jsonschema.Reflector
44+
4145
formDecoders map[rest.ParamIn]*form.Decoder
4246
decoderFunctions map[rest.ParamIn]decoderFunc
4347
defaultValDecoder *form.Decoder
@@ -257,7 +261,7 @@ func (df *DecoderFactory) jsonParams(formDecoder *form.Decoder, in rest.ParamIn,
257261
func (df *DecoderFactory) makeDefaultDecoder(input interface{}, m *decoder) {
258262
defaults := url.Values{}
259263

260-
refl.WalkFieldsRecursively(reflect.ValueOf(input), func(_ reflect.Value, sf reflect.StructField, path []reflect.StructField) {
264+
refl.WalkFieldsRecursively(reflect.ValueOf(input), func(v reflect.Value, sf reflect.StructField, path []reflect.StructField) {
261265
var key string
262266

263267
for _, p := range path {
@@ -278,8 +282,24 @@ func (df *DecoderFactory) makeDefaultDecoder(input interface{}, m *decoder) {
278282
key += "[" + sf.Name + "]"
279283
}
280284

281-
if d, ok := sf.Tag.Lookup(defaultTag); ok {
285+
if d, ok := sf.Tag.Lookup(defaultTag); ok { //nolint:nestif
282286
defaults[key] = []string{d}
287+
} else if df.JSONSchemaReflector != nil {
288+
vi := v.Interface()
289+
290+
s, err := df.JSONSchemaReflector.Reflect(vi)
291+
if err != nil {
292+
panic(err.Error())
293+
}
294+
295+
if s.Default != nil {
296+
d, err := json.Marshal(s.Default)
297+
if err != nil {
298+
panic(err.Error())
299+
}
300+
301+
defaults[key] = []string{strings.Trim(string(d), `"`)}
302+
}
283303
}
284304
})
285305

request/factory_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package request_test
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
67
"net/http"
@@ -10,6 +11,7 @@ import (
1011

1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
14+
"github.com/swaggest/jsonschema-go"
1315
"github.com/swaggest/rest"
1416
"github.com/swaggest/rest/request"
1517
)
@@ -269,3 +271,68 @@ func TestDecoderFactory_MakeDecoder_header_case_sensitivity(t *testing.T) {
269271
assert.Equal(t, "hello!", v.C)
270272
assert.Equal(t, "hello!", v.D)
271273
}
274+
275+
type defaultFromSchema string
276+
277+
func (d *defaultFromSchema) PrepareJSONSchema(schema *jsonschema.Schema) error {
278+
schema.WithDefault(enum1)
279+
schema.WithTitle("Value with default from schema")
280+
281+
return nil
282+
}
283+
284+
type defaultFromSchemaVal string
285+
286+
func (d defaultFromSchemaVal) PrepareJSONSchema(schema *jsonschema.Schema) error {
287+
schema.WithDefault(enum1)
288+
schema.WithTitle("Value with default from schema")
289+
290+
return nil
291+
}
292+
293+
const (
294+
enum1 = "all"
295+
enum2 = "none"
296+
)
297+
298+
func (d *defaultFromSchema) Enum() []interface{} {
299+
return []interface{}{enum1, enum2}
300+
}
301+
302+
func (d defaultFromSchemaVal) Enum() []interface{} {
303+
return []interface{}{enum1, enum2}
304+
}
305+
306+
func TestNewDecoderFactory_default(t *testing.T) {
307+
type NewThing struct {
308+
DefaultedQuery *defaultFromSchema `query:"dq"`
309+
DefaultedPtr *defaultFromSchema `json:"dp,omitempty"`
310+
Defaulted defaultFromSchema `json:"d"`
311+
DefaultedTag defaultFromSchema `query:"dt" default:"none"`
312+
DefaultedQueryVal *defaultFromSchemaVal `query:"dqv"`
313+
DefaultedPtrVal *defaultFromSchemaVal `json:"dpv,omitempty"`
314+
DefaultedVal defaultFromSchemaVal `json:"dv"`
315+
DefaultedTagVal defaultFromSchemaVal `query:"dtv" default:"none"`
316+
}
317+
318+
df := request.NewDecoderFactory()
319+
df.ApplyDefaults = true
320+
df.JSONSchemaReflector = &jsonschema.Reflector{}
321+
322+
var input NewThing
323+
dec := df.MakeDecoder(http.MethodPost, input, nil)
324+
325+
req, err := http.NewRequest(http.MethodPost, "/foo", bytes.NewReader([]byte(`{}`)))
326+
require.NoError(t, err)
327+
328+
require.NoError(t, dec.Decode(req, &input, nil))
329+
assert.Equal(t, enum1, string(*input.DefaultedPtr))
330+
assert.Equal(t, enum1, string(input.Defaulted))
331+
assert.Equal(t, enum1, string(*input.DefaultedQuery))
332+
assert.Equal(t, enum2, string(input.DefaultedTag))
333+
334+
assert.Equal(t, enum1, string(*input.DefaultedPtrVal))
335+
assert.Equal(t, enum1, string(input.DefaultedVal))
336+
assert.Equal(t, enum1, string(*input.DefaultedQueryVal))
337+
assert.Equal(t, enum2, string(input.DefaultedTagVal))
338+
}

web/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func NewService(refl oapi.Reflector, options ...func(s *Service)) *Service {
4444
if s.DecoderFactory == nil {
4545
decoderFactory := request.NewDecoderFactory()
4646
decoderFactory.ApplyDefaults = true
47+
decoderFactory.JSONSchemaReflector = s.OpenAPICollector.Refl().JSONSchemaReflector()
4748
decoderFactory.SetDecoderFunc(rest.ParamInPath, chirouter.PathToURLValues)
4849

4950
s.DecoderFactory = decoderFactory

0 commit comments

Comments
 (0)