Skip to content

Commit

Permalink
Improve example inferring with JSON support and type cohesion (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop authored Mar 12, 2024
1 parent 9575eb9 commit 524f24b
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ linters-settings:
check-exported: false
unparam:
check-exported: true
nestif:
min-complexity: 7

linters:
enable-all: true
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ There are a few interfaces that can be implemented on a type to customize JSON S
* [`Titled`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#Titled) exposes title.
* [`Enum`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#Enum) exposes enum values.
* [`NamedEnum`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#NamedEnum) exposes enum values with names.
* [`SchemaInliner`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#SchemaInliner) inlines schema without creating a definition.
* [`IgnoreTypeName`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#IgnoreTypeName), when implemented on a mapped type forces the use of original type for definition name.
* [`EmbedReferencer`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#EmbedReferencer), when implemented on an embedded field type, makes an `allOf` reference to that type definition.

And a few interfaces to expose subschemas (`anyOf`, `allOf`, `oneOf`, `not` and `if`, `then`, `else`).
* [`AnyOfExposer`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#AnyOfExposer) exposes `anyOf` subschemas.
Expand All @@ -157,6 +160,8 @@ There are also helper functions
[`jsonschema.OneOf`](https://pkg.go.dev/github.com/swaggest/jsonschema-go#OneOf)
to create exposer instance from multiple values.



### Configuring the reflector

Additional centralized configuration is available with
Expand Down
69 changes: 15 additions & 54 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,12 @@ func (r *Reflector) walkProperties(v reflect.Value, parent *Schema, rc *ReflectC
parent.AdditionalProperties = &SchemaOrBool{TypeBoolean: additionalProperties}
}

if !rc.SkipNonConstraints {
if err := reflectExamples(rc, parent, field); err != nil {
return err
}
}

continue
}

Expand Down Expand Up @@ -1144,7 +1150,7 @@ func (r *Reflector) walkProperties(v reflect.Value, parent *Schema, rc *ReflectC
}

if !rc.SkipNonConstraints {
if err := reflectExamples(&propertySchema, field); err != nil {
if err := reflectExamples(rc, &propertySchema, field); err != nil {
return err
}
}
Expand Down Expand Up @@ -1325,8 +1331,8 @@ func checkNullability(propertySchema *Schema, rc *ReflectContext, ft reflect.Typ
}
}

func reflectExamples(propertySchema *Schema, field reflect.StructField) error {
if err := reflectExample(propertySchema, field); err != nil {
func reflectExamples(rc *ReflectContext, propertySchema *Schema, field reflect.StructField) error {
if err := reflectExample(rc, propertySchema, field); err != nil {
return err
}

Expand All @@ -1345,57 +1351,12 @@ func reflectExamples(propertySchema *Schema, field reflect.StructField) error {
return nil
}

func reflectExample(propertySchema *Schema, field reflect.StructField) error {
var val interface{}

switch {
case propertySchema.HasType(String):
var example *string

refl.ReadStringPtrTag(field.Tag, "example", &example)

if example != nil {
val = *example
}
case propertySchema.HasType(Number) && val == nil:
var example *float64

if err := refl.ReadFloatPtrTag(field.Tag, "example", &example); err != nil {
return err
}

if example != nil {
val = *example
}
case propertySchema.HasType(Integer) && val == nil:
var example *int64

if err := refl.ReadIntPtrTag(field.Tag, "example", &example); err != nil {
return err
}

if example != nil {
val = *example
}
case propertySchema.HasType(Boolean) && val == nil:
var example *bool

if err := refl.ReadBoolPtrTag(field.Tag, "example", &example); err != nil {
return err
}

if example != nil {
val = *example
}
case propertySchema.HasType(Array) && val == nil && propertySchema.Items != nil &&
propertySchema.Items.SchemaOrBool != nil && propertySchema.Items.SchemaOrBool.TypeObject != nil:
return reflectExample(propertySchema.Items.SchemaOrBool.TypeObject, field)
default:
return nil
}

if val != nil {
propertySchema.Examples = append(propertySchema.Examples, val)
func reflectExample(rc *ReflectContext, propertySchema *Schema, field reflect.StructField) error {
err := checkInlineValue(propertySchema, field, "example", func(i interface{}) *Schema {
return propertySchema.WithExamples(i)
})
if err != nil {
return fmt.Errorf("%s: %w", strings.Join(append(rc.Path[1:], field.Name), "."), err)
}

return nil
Expand Down
48 changes: 42 additions & 6 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -980,10 +980,32 @@ 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_parentTagsExample(t *testing.T) {
type Test struct {
Foo string `json:"foo" query:"foo"`
_ struct{} `title:"Test" example:"{\"foo\":\"abc\"}"` // 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{})
require.NoError(t, err)

assertjson.EqualMarshal(t, []byte(`{
"title":"Test","description":"This is a test.","examples":[{"foo":"abc"}],
"additionalProperties":false,"properties":{"foo":{"type":"string"}},
"type":"object"
}`), s)
}

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.
_ struct{} `title:"Test" example:"{\"foo\":\"abc\"}"` // 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.
Expand All @@ -992,7 +1014,16 @@ func TestReflector_Reflect_parentTagsFiltered(t *testing.T) {

r := jsonschema.Reflector{}

s, err := r.Reflect(Test{}, func(rc *jsonschema.ReflectContext) {
s, err := r.Reflect(Test{})
require.NoError(t, err)

assertjson.EqualMarshal(t, []byte(`{
"title":"Test","description":"This is a test.","examples":[{"foo":"abc"}],
"additionalProperties":false,"properties":{"foo":{"type":"string"}},
"type":"object"
}`), s)

s, err = r.Reflect(Test{}, func(rc *jsonschema.ReflectContext) {
rc.UnnamedFieldWithTag = true
rc.PropertyNameTag = "json"
})
Expand Down Expand Up @@ -1228,7 +1259,7 @@ func TestReflector_Reflect_skipNonConstraints(t *testing.T) {
func TestReflector_Reflect_examples(t *testing.T) {
type WantExample struct {
A string `json:"a" example:"example of a"`
B []string `json:"b" example:"example of b"`
B []string `json:"b" example:"[\"example of b\"]"`
C int `json:"c" examples:"[\"foo\", 2, 3]" example:"123"`
}

Expand All @@ -1240,7 +1271,7 @@ func TestReflector_Reflect_examples(t *testing.T) {
"properties":{
"a":{"examples":["example of a"],"type":"string"},
"b":{
"items":{"examples":["example of b"],"type":"string"},
"examples":[["example of b"]],"items":{"type":"string"},
"type":["array","null"]
},
"c":{"examples":[123,"foo",2,3],"type":"integer"}
Expand All @@ -1253,7 +1284,7 @@ func TestReflector_Reflect_namedSlice(t *testing.T) {
type PanicType []string

type PanicStruct struct {
IPPolicy PanicType `json:"ip_policy" example:"127.0.0.1"`
IPPolicy PanicType `json:"ip_policy" example:"[\"127.0.0.1\"]"`
}

reflector := jsonschema.Reflector{}
Expand All @@ -1264,7 +1295,12 @@ func TestReflector_Reflect_namedSlice(t *testing.T) {
"definitions":{
"JsonschemaGoTestPanicType":{"items":{"type":"string"},"type":["array","null"]}
},
"properties":{"ip_policy":{"$ref":"#/definitions/JsonschemaGoTestPanicType"}},
"properties":{
"ip_policy":{
"$ref":"#/definitions/JsonschemaGoTestPanicType",
"examples":[["127.0.0.1"]]
}
},
"type":"object"
}`), schema)
}
Expand Down

0 comments on commit 524f24b

Please sign in to comment.