Skip to content

Commit

Permalink
Add control to filter tags of unnamed fields (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Apr 14, 2022
1 parent 2fb3e29 commit 21e261d
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 2 deletions.
5 changes: 5 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ type ReflectContext struct {
// ProcessWithoutTags enables processing fields without any tags specified.
ProcessWithoutTags bool

// UnnamedFieldWithTag enables a requirement that name tag is present
// when processing _ fields to set up parent schema, e.g.
// _ struct{} `header:"_" additionalProperties:"false"`.
UnnamedFieldWithTag bool

// EnvelopNullability enables `anyOf` enveloping of "type":"null" instead of injecting into definition.
EnvelopNullability bool

Expand Down
13 changes: 11 additions & 2 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,21 @@ func checkSchemaSetup(v reflect.Value, s *Schema) (bool, error) {
// _ struct{} `additionalProperties:"false" description:"MyObj is my object."`
// }
//
// In case of a structure with multiple name tags, you can enable filtering of unnamed fields with
// ReflectContext.UnnamedFieldWithTag option and add matching name tags to structure (e.g. query:"_").
// type MyObj struct {
// BoundedNumber int `query:"boundedNumber" minimum:"-100" maximum:"100"`
// SpecialString string `json:"specialString" pattern:"^[a-z]{4}$" minLength:"4" maxLength:"4"`
// // These parent schema tags would only be applied to `query` schema reflection (not for `json`).
// _ struct{} `query:"_" additionalProperties:"false" description:"MyObj is my object."`
// }
//
// Additionally there are structure can implement any of special interfaces for fine-grained Schema control:
// RawExposer, Exposer, Preparer.
//
// These interfaces allow exposing particular schema keywords:
// Titled, Described, Enum, NamedEnum.
func (r *Reflector) Reflect(i interface{}, options ...func(*ReflectContext)) (Schema, error) {
func (r *Reflector) Reflect(i interface{}, options ...func(rc *ReflectContext)) (Schema, error) {
rc := ReflectContext{}
rc.DefinitionsPrefix = "#/definitions/"
rc.PropertyNameTag = "json"
Expand Down Expand Up @@ -712,7 +721,7 @@ func (r *Reflector) walkProperties(v reflect.Value, parent *Schema, rc *ReflectC
}

// Use unnamed fields to configure parent schema.
if field.Name == "_" {
if field.Name == "_" && (!rc.UnnamedFieldWithTag || tagFound) {
if err := refl.PopulateFieldsFromTags(parent, field.Tag); err != nil {
return err
}
Expand Down
45 changes: 45 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,3 +1025,48 @@ func TestReflector_Reflect_parentTags(t *testing.T) {
}{})
assert.EqualError(t, err, "failed to parse int value abc in tag minProperties: strconv.ParseInt: parsing \"abc\": invalid syntax")
}

func TestReflector_Reflect_parentTagsFiltered(t *testing.T) {
type Test struct {
Foo string `json:"foo" query:"foo"`
_ struct{} `title:"Test"` // Tags of unnamed field are applied to parent schema.

// There can be more than one field to set up parent schema.
// Types of such fields are not relevant, only tags matter.
_ string `query:"_" additionalProperties:"false" description:"This is a test."`
}

r := jsonschema.Reflector{}

s, err := r.Reflect(Test{}, func(rc *jsonschema.ReflectContext) {
rc.UnnamedFieldWithTag = true
rc.PropertyNameTag = "json"
})
assert.NoError(t, err)

// No parent schema update for json, as tag is missing in unnamed field.
assertjson.EqualMarshal(t, []byte(`{"properties":{"foo":{"type":"string"}},"type":"object"}`), s)

s, err = r.Reflect(Test{}, func(rc *jsonschema.ReflectContext) {
rc.UnnamedFieldWithTag = true
rc.PropertyNameTag = "query"
})
assert.NoError(t, err)

// Parent schema is updated for query, as tag is present in unnamed field.
assertjson.EqualMarshal(t, []byte(`{
"description":"This is a test.","additionalProperties":false,
"properties":{"foo":{"type":"string"}},"type":"object"
}`), s)

// Failure scenarios.
_, err = r.Reflect(struct {
_ string `additionalProperties:"abc"`
}{})
assert.EqualError(t, err, "failed to parse bool value abc in tag additionalProperties: strconv.ParseBool: parsing \"abc\": invalid syntax")

_, err = r.Reflect(struct {
_ string `minProperties:"abc"`
}{})
assert.EqualError(t, err, "failed to parse int value abc in tag minProperties: strconv.ParseInt: parsing \"abc\": invalid syntax")
}

0 comments on commit 21e261d

Please sign in to comment.