From 8d20c56a8c476007d2673ef7962c5b1e5988b405 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 4 Mar 2021 07:19:44 +0100 Subject: [PATCH 1/5] nits Signed-off-by: Pierre Fenoll --- openapi3gen/openapi3gen.go | 19 ++++++------------- openapi3gen/openapi3gen_test.go | 11 +++++++---- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 4a80405ba..39e1de851 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -1,4 +1,4 @@ -// Package openapi3gen generates OpenAPI 3 schemas for Go types. +// Package openapi3gen generates OpenAPIv3 JSON schemas from Go types. package openapi3gen import ( @@ -48,8 +48,7 @@ func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, erro } func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) { - ref := g.Types[t] - if ref != nil { + if ref := g.Types[t]; ref != nil { g.SchemaRefs[ref]++ return ref, nil } @@ -62,7 +61,6 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect } func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) { - // Get TypeInfo typeInfo := jsoninfo.GetTypeInfo(t) for _, parent := range parents { if parent == typeInfo { @@ -70,19 +68,15 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } - // Doesn't exist. - // Create the schema. if cap(parents) == 0 { parents = make([]*jsoninfo.TypeInfo, 0, 4) } parents = append(parents, typeInfo) - // Ignore pointers for t.Kind() == reflect.Ptr { t = t.Elem() } - // Create instance if strings.HasSuffix(t.Name(), "Ref") { _, a := t.FieldByName("Ref") v, b := t.FieldByName("Value") @@ -104,12 +98,12 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } - // Allocate schema schema := &openapi3.Schema{} switch t.Kind() { case reflect.Func, reflect.Chan: - return nil, nil + return nil, nil // ignore + case reflect.Bool: schema.Type = "boolean" @@ -128,9 +122,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec case reflect.Slice: if t.Elem().Kind() == reflect.Uint8 { if t == rawMessageType { - return &openapi3.SchemaRef{ - Value: schema, - }, nil + return &openapi3.SchemaRef{Value: schema}, nil } schema.Type = "string" schema.Format = "byte" @@ -183,6 +175,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } } } + return openapi3.NewSchemaRef(t.Name(), schema), nil } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 2a58433cb..114783034 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -33,20 +33,23 @@ func TestSimple(t *testing.T) { Bytes []byte `json:"bytes"` JSON json.RawMessage `json:"json"` Time time.Time `json:"time"` - Slice []*ExampleChild `json:"slice"` + Slice []ExampleChild `json:"slice"` Map map[string]*ExampleChild `json:"map"` - Struct struct { + + Struct struct { X string `json:"x"` } `json:"struct"` + EmptyStruct struct { - X string + Y string } `json:"structWithoutFields"` + Ptr *ExampleChild `json:"ptr"` } schema, refsMap, err := NewSchemaRefForValue(&Example{}) require.NoError(t, err) - require.Len(t, refsMap, 14) + require.Len(t, refsMap, 15) data, err := json.Marshal(schema) require.NoError(t, err) require.JSONEq(t, expectedSimple, string(data)) From b8c8d4334895f07d211c79c95e8175a554207fc3 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 4 Mar 2021 07:49:54 +0100 Subject: [PATCH 2/5] extract an example Signed-off-by: Pierre Fenoll --- openapi3gen/openapi3gen_test.go | 93 --------------------------- openapi3gen/simple_test.go | 109 ++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 93 deletions(-) create mode 100644 openapi3gen/simple_test.go diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 114783034..d75506447 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,9 +1,7 @@ package openapi3gen import ( - "encoding/json" "testing" - "time" "github.com/stretchr/testify/require" ) @@ -21,94 +19,3 @@ func TestCyclic(t *testing.T) { require.Nil(t, schema) require.Empty(t, refsMap) } - -func TestSimple(t *testing.T) { - type ExampleChild string - type Example struct { - Bool bool `json:"bool"` - Int int `json:"int"` - Int64 int64 `json:"int64"` - Float64 float64 `json:"float64"` - String string `json:"string"` - Bytes []byte `json:"bytes"` - JSON json.RawMessage `json:"json"` - Time time.Time `json:"time"` - Slice []ExampleChild `json:"slice"` - Map map[string]*ExampleChild `json:"map"` - - Struct struct { - X string `json:"x"` - } `json:"struct"` - - EmptyStruct struct { - Y string - } `json:"structWithoutFields"` - - Ptr *ExampleChild `json:"ptr"` - } - - schema, refsMap, err := NewSchemaRefForValue(&Example{}) - require.NoError(t, err) - require.Len(t, refsMap, 15) - data, err := json.Marshal(schema) - require.NoError(t, err) - require.JSONEq(t, expectedSimple, string(data)) -} - -const expectedSimple = ` -{ - "type": "object", - "properties": { - "bool": { - "type": "boolean" - }, - "int": { - "type": "integer", - "format": "int64" - }, - "int64": { - "type": "integer", - "format": "int64" - }, - "float64": { - "type": "number" - }, - "time": { - "type": "string", - "format": "date-time" - }, - "string": { - "type": "string" - }, - "bytes": { - "type": "string", - "format": "byte" - }, - "json": {}, - "slice": { - "type": "array", - "items": { - "type": "string" - } - }, - "map": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "struct": { - "type": "object", - "properties": { - "x": { - "type": "string" - } - } - }, - "structWithoutFields": {}, - "ptr": { - "type": "string" - } - } -} -` diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go new file mode 100644 index 000000000..210e1d1fe --- /dev/null +++ b/openapi3gen/simple_test.go @@ -0,0 +1,109 @@ +package openapi3gen_test + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/getkin/kin-openapi/openapi3gen" +) + +type ( + SomeStruct struct { + Bool bool `json:"bool"` + Int int `json:"int"` + Int64 int64 `json:"int64"` + Float64 float64 `json:"float64"` + String string `json:"string"` + Bytes []byte `json:"bytes"` + JSON json.RawMessage `json:"json"` + Time time.Time `json:"time"` + Slice []SomeOtherType `json:"slice"` + Map map[string]*SomeOtherType `json:"map"` + + Struct struct { + X string `json:"x"` + } `json:"struct"` + + EmptyStruct struct { + Y string + } `json:"structWithoutFields"` + + Ptr *SomeOtherType `json:"ptr"` + } + + SomeOtherType string +) + +func Example() { + schema, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}) + if err != nil { + panic(err) + } + + if len(refsMap) != 15 { + panic(fmt.Sprintf("unintended len(refsMap) = %d", len(refsMap))) + } + + data, err := json.MarshalIndent(schema, "", " ") + if err != nil { + panic(err) + } + fmt.Printf("%s\n", data) + // Output: + // { + // "properties": { + // "bool": { + // "type": "boolean" + // }, + // "bytes": { + // "format": "byte", + // "type": "string" + // }, + // "float64": { + // "type": "number" + // }, + // "int": { + // "format": "int64", + // "type": "integer" + // }, + // "int64": { + // "format": "int64", + // "type": "integer" + // }, + // "json": {}, + // "map": { + // "additionalProperties": { + // "type": "string" + // }, + // "type": "object" + // }, + // "ptr": { + // "type": "string" + // }, + // "slice": { + // "items": { + // "type": "string" + // }, + // "type": "array" + // }, + // "string": { + // "type": "string" + // }, + // "struct": { + // "properties": { + // "x": { + // "type": "string" + // } + // }, + // "type": "object" + // }, + // "structWithoutFields": {}, + // "time": { + // "format": "date-time", + // "type": "string" + // } + // }, + // "type": "object" + // } +} From fbe30cc9f7b187ef40ae05212eda55b48d5169ed Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 4 Mar 2021 09:13:23 +0100 Subject: [PATCH 3/5] introduce openapi3gen.Option Signed-off-by: Pierre Fenoll --- jsoninfo/field_info.go | 14 ++++------ openapi3gen/openapi3gen.go | 45 ++++++++++++++++++++++++++++----- openapi3gen/openapi3gen_test.go | 24 ++++++++++++++++-- openapi3gen/simple_test.go | 4 +-- 4 files changed, 67 insertions(+), 20 deletions(-) diff --git a/jsoninfo/field_info.go b/jsoninfo/field_info.go index d949a79d3..d2ad505bd 100644 --- a/jsoninfo/field_info.go +++ b/jsoninfo/field_info.go @@ -63,7 +63,7 @@ iteration: // Read our custom "multijson" tag that // allows multiple fields with the same name. - if v := f.Tag.Get("multijson"); len(v) > 0 { + if v := f.Tag.Get("multijson"); v != "" { field.MultipleFields = true jsonTag = v } @@ -74,11 +74,11 @@ iteration: } // Parse the tag - if len(jsonTag) > 0 { + if jsonTag != "" { field.HasJSONTag = true for i, part := range strings.Split(jsonTag, ",") { if i == 0 { - if len(part) > 0 { + if part != "" { field.JSONName = part } } else { @@ -92,12 +92,8 @@ iteration: } } - if _, ok := field.Type.MethodByName("MarshalJSON"); ok { - field.TypeIsMarshaller = true - } - if _, ok := field.Type.MethodByName("UnmarshalJSON"); ok { - field.TypeIsUnmarshaller = true - } + _, field.TypeIsMarshaller = field.Type.MethodByName("MarshalJSON") + _, field.TypeIsUnmarshaller = field.Type.MethodByName("UnmarshalJSON") // Field is done fields = append(fields, field) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 39e1de851..82e4a9e6c 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -18,8 +18,22 @@ func (err *CycleError) Error() string { return "Detected JSON cycle" } -func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { - g := NewGenerator() +// Option allows tweaking SchemaRef generation +type Option func(*generatorOpt) + +type generatorOpt struct { + useAllExportedFields bool +} + +// UseAllExportedFields changes the default behavior of only +// generating schemas for struct fields with a JSON tag. +func UseAllExportedFields() Option { + return func(x *generatorOpt) { x.useAllExportedFields = true } +} + +// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef. +func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) { + g := NewGenerator(opts...) ref, err := g.GenerateSchemaRef(reflect.TypeOf(value)) for ref := range g.SchemaRefs { ref.Ref = "" @@ -28,6 +42,8 @@ func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3 } type Generator struct { + opts generatorOpt + Types map[reflect.Type]*openapi3.SchemaRef // SchemaRefs contains all references and their counts. @@ -36,14 +52,20 @@ type Generator struct { SchemaRefs map[*openapi3.SchemaRef]int } -func NewGenerator() *Generator { +func NewGenerator(opts ...Option) *Generator { + gOpt := &generatorOpt{} + for _, f := range opts { + f(gOpt) + } return &Generator{ Types: make(map[reflect.Type]*openapi3.SchemaRef), SchemaRefs: make(map[*openapi3.SchemaRef]int), + opts: *gOpt, } } func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) { + //check generatorOpt consistency here return g.generateSchemaRefFor(nil, t) } @@ -155,17 +177,26 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Format = "date-time" } else { for _, fieldInfo := range typeInfo.Fields { - // Only fields with JSON tag are considered - if !fieldInfo.HasJSONTag { + // Only fields with JSON tag are considered (by default) + if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields { continue } - ref, err := g.generateSchemaRefFor(parents, fieldInfo.Type) + // If asked, try to use yaml tag + name, fType := fieldInfo.JSONName, fieldInfo.Type + if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields { + ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + if yamlTag := ff.Tag.Get("yaml"); yamlTag != "" { + name, fType = yamlTag, ff.Type + } + } + + ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { return nil, err } if ref != nil { g.SchemaRefs[ref]++ - schema.WithPropertyRef(fieldInfo.JSONName, ref) + schema.WithPropertyRef(name, ref) } } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index d75506447..1422017a5 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -3,6 +3,7 @@ package openapi3gen import ( "testing" + "github.com/getkin/kin-openapi/openapi3" "github.com/stretchr/testify/require" ) @@ -14,8 +15,27 @@ type CyclicType1 struct { } func TestCyclic(t *testing.T) { - schema, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) + schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) require.IsType(t, &CycleError{}, err) - require.Nil(t, schema) + require.Nil(t, schemaRef) require.Empty(t, refsMap) } + +func TestExportedNonTagged(t *testing.T) { + type Bla struct { + A string + Another string `json:"another"` + yetAnother string + EvenAYaml string `yaml:"even_a_yaml"` + } + + schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields()) + require.NoError(t, err) + require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ + Type: "object", + Properties: map[string]*openapi3.SchemaRef{ + "A": {Value: &openapi3.Schema{Type: "string"}}, + "another": {Value: &openapi3.Schema{Type: "string"}}, + "even_a_yaml": {Value: &openapi3.Schema{Type: "string"}}, + }}}, schemaRef) +} diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go index 210e1d1fe..46fb66ebd 100644 --- a/openapi3gen/simple_test.go +++ b/openapi3gen/simple_test.go @@ -36,7 +36,7 @@ type ( ) func Example() { - schema, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}) + schemaRef, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}) if err != nil { panic(err) } @@ -45,7 +45,7 @@ func Example() { panic(fmt.Sprintf("unintended len(refsMap) = %d", len(refsMap))) } - data, err := json.MarshalIndent(schema, "", " ") + data, err := json.MarshalIndent(schemaRef, "", " ") if err != nil { panic(err) } From e4b79ce442893218cafffde3202d781209cba4bd Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 4 Mar 2021 09:36:55 +0100 Subject: [PATCH 4/5] generate more accurate numeric schemas Signed-off-by: Pierre Fenoll --- openapi3gen/openapi3gen.go | 50 +++++++++++++++++++++++++++++++++++--- openapi3gen/simple_test.go | 2 +- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 82e4a9e6c..7bc4adad1 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -3,6 +3,7 @@ package openapi3gen import ( "encoding/json" + "math" "reflect" "strings" "time" @@ -129,14 +130,45 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec case reflect.Bool: schema.Type = "boolean" - case reflect.Int, - reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + case reflect.Int: + schema.Type = "integer" + case reflect.Int8: + schema.Type = "integer" + schema.Min = &minInt8 + schema.Max = &maxInt8 + case reflect.Int16: + schema.Type = "integer" + schema.Min = &minInt16 + schema.Max = &maxInt16 + case reflect.Int32: + schema.Type = "integer" + schema.Format = "int32" + case reflect.Int64: schema.Type = "integer" schema.Format = "int64" + case reflect.Uint8: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint8 + case reflect.Uint16: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint16 + case reflect.Uint32: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint32 + case reflect.Uint64: + schema.Type = "integer" + schema.Min = &zeroInt + schema.Max = &maxUint64 - case reflect.Float32, reflect.Float64: + case reflect.Float32: schema.Type = "number" + schema.Format = "float" + case reflect.Float64: + schema.Type = "number" + schema.Format = "double" case reflect.String: schema.Type = "string" @@ -216,4 +248,14 @@ var RefSchemaRef = openapi3.NewSchemaRef("Ref", var ( timeType = reflect.TypeOf(time.Time{}) rawMessageType = reflect.TypeOf(json.RawMessage{}) + + zeroInt = float64(0) + maxInt8 = float64(math.MaxInt8) + minInt8 = float64(math.MinInt8) + maxInt16 = float64(math.MaxInt16) + minInt16 = float64(math.MinInt16) + maxUint8 = float64(math.MaxUint8) + maxUint16 = float64(math.MaxUint16) + maxUint32 = float64(math.MaxUint32) + maxUint64 = float64(math.MaxUint64) ) diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go index 46fb66ebd..d997e23b2 100644 --- a/openapi3gen/simple_test.go +++ b/openapi3gen/simple_test.go @@ -61,10 +61,10 @@ func Example() { // "type": "string" // }, // "float64": { + // "format": "double", // "type": "number" // }, // "int": { - // "format": "int64", // "type": "integer" // }, // "int64": { From 3d03d2de9daae69dff08cf36afbae8744f405094 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 4 Mar 2021 09:43:38 +0100 Subject: [PATCH 5/5] nit Signed-off-by: Pierre Fenoll --- openapi3gen/openapi3gen.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 7bc4adad1..4cf022e52 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -217,8 +217,8 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec name, fType := fieldInfo.JSONName, fieldInfo.Type if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields { ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) - if yamlTag := ff.Tag.Get("yaml"); yamlTag != "" { - name, fType = yamlTag, ff.Type + if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { + name, fType = tag, ff.Type } }