From 4e7b01fe32a921ce50afbf11d88b26d3e5162024 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Tue, 27 Jul 2021 10:56:35 -0400 Subject: [PATCH 1/7] Detect if a field is anonymous and handle the indirection --- openapi3gen/openapi3gen.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 960698324..84d1f998d 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -217,9 +217,21 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec // 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 tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { - name, fType = tag, ff.Type + // Handle anonymous fields/embedded structs + if t.Field(fieldInfo.Index[0]).Anonymous { + ref, err := g.generateSchemaRefFor(parents, fType) + if err != nil { + return nil, err + } + if ref != nil { + g.SchemaRefs[ref]++ + schema.WithPropertyRef(name, ref) + } + } else { + ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1]) + if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" { + name, fType = tag, ff.Type + } } } From 5e49b036ea84401add56c9daca5a14c1e7ee7f65 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Wed, 28 Jul 2021 10:25:23 -0400 Subject: [PATCH 2/7] Added unit test for embedded structs --- openapi3gen/openapi3gen_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 777bb9dad..f8e2430e2 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -1,6 +1,7 @@ package openapi3gen import ( + "reflect" "testing" "github.com/getkin/kin-openapi/openapi3" @@ -53,3 +54,33 @@ func TestExportUint(t *testing.T) { "uint": {Value: &openapi3.Schema{Type: "integer", Min: &zeroInt}}, }}}, schemaRef) } + +func TestEmbeddedStructs(t *testing.T) { + type EmbeddedStruct struct { + ID string + } + + type ContainerStruct struct { + Name string + EmbeddedStruct + } + + instance := &ContainerStruct{ + Name: "Container", + EmbeddedStruct: EmbeddedStruct{ + ID: "Embedded", + }, + } + + generator := NewGenerator(UseAllExportedFields()) + + schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) + require.NoError(t, err) + + var ok bool + _, ok = schemaRef.Value.Properties["Name"] + require.Equal(t, true, ok) + + _, ok = schemaRef.Value.Properties["ID"] + require.Equal(t, true, ok) +} From 2f4db06a1cc71422683f4cec3de4eb8f7f22d758 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Thu, 29 Jul 2021 12:16:34 -0400 Subject: [PATCH 3/7] Add support for cyclical references --- openapi3gen/openapi3gen.go | 42 +++++++++++++++++++++++++++--- openapi3gen/openapi3gen_test.go | 45 +++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 84d1f998d..26e214eb4 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -3,6 +3,7 @@ package openapi3gen import ( "encoding/json" + "fmt" "math" "reflect" "strings" @@ -104,6 +105,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec if a && b { vs, err := g.generateSchemaRefFor(parents, v.Type) if err != nil { + // TODO: this needs code review + if _, ok := err.(*CycleError); ok { + g.SchemaRefs[vs]++ + return vs, nil + } return nil, err } refSchemaRef := RefSchemaRef @@ -185,7 +191,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Type = "array" items, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok { + items = g.getCycleSchemaRef(t.Elem(), schema) + } else { + return nil, err + } } if items != nil { g.SchemaRefs[items]++ @@ -197,7 +207,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Type = "object" additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok { + additionalProperties = g.getCycleSchemaRef(t.Elem(), schema) + } else { + return nil, err + } } if additionalProperties != nil { g.SchemaRefs[additionalProperties]++ @@ -221,7 +235,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec if t.Field(fieldInfo.Index[0]).Anonymous { ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok { + ref = g.getCycleSchemaRef(fType, schema) + } else { + return nil, err + } } if ref != nil { g.SchemaRefs[ref]++ @@ -237,7 +255,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { - return nil, err + if _, ok := err.(*CycleError); ok { + ref = g.getCycleSchemaRef(fType, schema) + } else { + return nil, err + } } if ref != nil { g.SchemaRefs[ref]++ @@ -255,6 +277,18 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec return openapi3.NewSchemaRef(t.Name(), schema), nil } +func (g *Generator) getCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { + var typeName string + switch t.Kind() { + case reflect.Ptr, reflect.Slice, reflect.Map: + return g.getCycleSchemaRef(t.Elem(), schema) + default: + typeName = t.Name() + } + + return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema) +} + var RefSchemaRef = openapi3.NewSchemaRef("Ref", openapi3.NewObjectSchema().WithProperty("$ref", openapi3.NewStringSchema().WithMinLength(1))) diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index f8e2430e2..61f083b04 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -8,20 +8,6 @@ import ( "github.com/stretchr/testify/require" ) -type CyclicType0 struct { - CyclicField *CyclicType1 `json:"a"` -} -type CyclicType1 struct { - CyclicField *CyclicType0 `json:"b"` -} - -func TestCyclic(t *testing.T) { - schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}) - require.IsType(t, &CycleError{}, err) - require.Nil(t, schemaRef) - require.Empty(t, refsMap) -} - func TestExportedNonTagged(t *testing.T) { type Bla struct { A string @@ -84,3 +70,34 @@ func TestEmbeddedStructs(t *testing.T) { _, ok = schemaRef.Value.Properties["ID"] require.Equal(t, true, ok) } + +func TestCircularReferences(t *testing.T) { + type ObjectDiff struct { + FieldCycle *ObjectDiff + SliceCycle []*ObjectDiff + MapCycle map[*ObjectDiff]*ObjectDiff + } + + instance := &ObjectDiff{ + FieldCycle: nil, + SliceCycle: nil, + MapCycle: nil, + } + + generator := NewGenerator(UseAllExportedFields()) + + schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance)) + require.NoError(t, err) + + require.NotNil(t, schemaRef.Value.Properties["FieldCycle"]) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["FieldCycle"].Ref) + + require.NotNil(t, schemaRef.Value.Properties["SliceCycle"]) + require.Equal(t, "array", schemaRef.Value.Properties["SliceCycle"].Value.Type) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["SliceCycle"].Value.Items.Ref) + + require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) + require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) + +} From f0f2e2d6da92b45d65d61df8e4e662ea7a8d8f80 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Thu, 29 Jul 2021 12:22:55 -0400 Subject: [PATCH 4/7] Remove newlines at end of file --- openapi3gen/openapi3gen_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 611aaac43..61f083b04 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -100,4 +100,4 @@ func TestCircularReferences(t *testing.T) { require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) -} \ No newline at end of file +} From 2e8edda952de6dc577e4fa6475011ea5e95aa027 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Thu, 29 Jul 2021 12:45:29 -0400 Subject: [PATCH 5/7] Make naming conventions consistent with project --- openapi3gen/openapi3gen.go | 12 ++++++------ openapi3gen/openapi3gen_test.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 26e214eb4..2e1eb8159 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -192,7 +192,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec items, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { if _, ok := err.(*CycleError); ok { - items = g.getCycleSchemaRef(t.Elem(), schema) + items = g.genereateCycleSchemaRef(t.Elem(), schema) } else { return nil, err } @@ -208,7 +208,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { if _, ok := err.(*CycleError); ok { - additionalProperties = g.getCycleSchemaRef(t.Elem(), schema) + additionalProperties = g.genereateCycleSchemaRef(t.Elem(), schema) } else { return nil, err } @@ -236,7 +236,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { if _, ok := err.(*CycleError); ok { - ref = g.getCycleSchemaRef(fType, schema) + ref = g.genereateCycleSchemaRef(fType, schema) } else { return nil, err } @@ -256,7 +256,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { if _, ok := err.(*CycleError); ok { - ref = g.getCycleSchemaRef(fType, schema) + ref = g.genereateCycleSchemaRef(fType, schema) } else { return nil, err } @@ -277,11 +277,11 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec return openapi3.NewSchemaRef(t.Name(), schema), nil } -func (g *Generator) getCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { +func (g *Generator) genereateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { var typeName string switch t.Kind() { case reflect.Ptr, reflect.Slice, reflect.Map: - return g.getCycleSchemaRef(t.Elem(), schema) + return g.genereateCycleSchemaRef(t.Elem(), schema) default: typeName = t.Name() } diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 61f083b04..0169bfbcf 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -71,7 +71,7 @@ func TestEmbeddedStructs(t *testing.T) { require.Equal(t, true, ok) } -func TestCircularReferences(t *testing.T) { +func TestCyclicReferences(t *testing.T) { type ObjectDiff struct { FieldCycle *ObjectDiff SliceCycle []*ObjectDiff From e3e5589172ec11b42bc7f4c1dd044c72772c14e0 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Fri, 30 Jul 2021 12:47:08 -0400 Subject: [PATCH 6/7] Fix bug in creating cycle references for slices and maps --- openapi3gen/openapi3gen.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 2e1eb8159..f5323961c 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -192,7 +192,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec items, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { if _, ok := err.(*CycleError); ok { - items = g.genereateCycleSchemaRef(t.Elem(), schema) + items = g.generateCycleSchemaRef(t.Elem(), schema) } else { return nil, err } @@ -208,7 +208,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { if _, ok := err.(*CycleError); ok { - additionalProperties = g.genereateCycleSchemaRef(t.Elem(), schema) + additionalProperties = g.generateCycleSchemaRef(t.Elem(), schema) } else { return nil, err } @@ -236,7 +236,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { if _, ok := err.(*CycleError); ok { - ref = g.genereateCycleSchemaRef(fType, schema) + ref = g.generateCycleSchemaRef(fType, schema) } else { return nil, err } @@ -256,7 +256,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { if _, ok := err.(*CycleError); ok { - ref = g.genereateCycleSchemaRef(fType, schema) + ref = g.generateCycleSchemaRef(fType, schema) } else { return nil, err } @@ -277,11 +277,23 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec return openapi3.NewSchemaRef(t.Name(), schema), nil } -func (g *Generator) genereateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { +func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Schema) *openapi3.SchemaRef { var typeName string switch t.Kind() { - case reflect.Ptr, reflect.Slice, reflect.Map: - return g.genereateCycleSchemaRef(t.Elem(), schema) + case reflect.Ptr: + return g.generateCycleSchemaRef(t.Elem(), schema) + case reflect.Slice: + ref := g.generateCycleSchemaRef(t.Elem(), schema) + sliceSchema := openapi3.NewSchema() + sliceSchema.Type = "array" + sliceSchema.Items = ref + return openapi3.NewSchemaRef("", sliceSchema) + case reflect.Map: + ref := g.generateCycleSchemaRef(t.Elem(), schema) + mapSchema := openapi3.NewSchema() + mapSchema.Type = "object" + mapSchema.AdditionalProperties = ref + return openapi3.NewSchemaRef("", mapSchema) default: typeName = t.Name() } From 1fdcc379c74cf2e70b8da15bf257af8bfd32ef34 Mon Sep 17 00:00:00 2001 From: DerekStrickland Date: Sat, 31 Jul 2021 06:27:58 -0400 Subject: [PATCH 7/7] add option to throw error on cycles w/tests --- openapi3gen/openapi3gen.go | 18 ++++++++++++------ openapi3gen/openapi3gen_test.go | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index f5323961c..7c321fe7a 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -23,6 +23,7 @@ type Option func(*generatorOpt) type generatorOpt struct { useAllExportedFields bool + throwErrorOnCycle bool } // UseAllExportedFields changes the default behavior of only @@ -31,6 +32,12 @@ func UseAllExportedFields() Option { return func(x *generatorOpt) { x.useAllExportedFields = true } } +// ThrowErrorOnCycle changes the default behavior of creating cycle +// refs to instead error if a cycle is detected. +func ThrowErrorOnCycle() Option { + return func(x *generatorOpt) { x.throwErrorOnCycle = 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...) @@ -105,8 +112,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec if a && b { vs, err := g.generateSchemaRefFor(parents, v.Type) if err != nil { - // TODO: this needs code review - if _, ok := err.(*CycleError); ok { + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { g.SchemaRefs[vs]++ return vs, nil } @@ -191,7 +197,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Type = "array" items, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { - if _, ok := err.(*CycleError); ok { + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { items = g.generateCycleSchemaRef(t.Elem(), schema) } else { return nil, err @@ -207,7 +213,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec schema.Type = "object" additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem()) if err != nil { - if _, ok := err.(*CycleError); ok { + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { additionalProperties = g.generateCycleSchemaRef(t.Elem(), schema) } else { return nil, err @@ -235,7 +241,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec if t.Field(fieldInfo.Index[0]).Anonymous { ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { - if _, ok := err.(*CycleError); ok { + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { ref = g.generateCycleSchemaRef(fType, schema) } else { return nil, err @@ -255,7 +261,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec ref, err := g.generateSchemaRefFor(parents, fType) if err != nil { - if _, ok := err.(*CycleError); ok { + if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { ref = g.generateCycleSchemaRef(fType, schema) } else { return nil, err diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 0169bfbcf..0062e2e5f 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -8,6 +8,20 @@ import ( "github.com/stretchr/testify/require" ) +type CyclicType0 struct { + CyclicField *CyclicType1 `json:"a"` +} +type CyclicType1 struct { + CyclicField *CyclicType0 `json:"b"` +} + +func TestCyclic(t *testing.T) { + schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}, ThrowErrorOnCycle()) + require.IsType(t, &CycleError{}, err) + require.Nil(t, schemaRef) + require.Empty(t, refsMap) +} + func TestExportedNonTagged(t *testing.T) { type Bla struct { A string @@ -99,5 +113,4 @@ func TestCyclicReferences(t *testing.T) { require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) - }