Skip to content

Commit

Permalink
fix type name for generic structures
Browse files Browse the repository at this point in the history
  • Loading branch information
Misfits09 committed Jan 3, 2023
1 parent 8bc1e2b commit b553cf8
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
fetch-depth: 2
- uses: actions/setup-go@v2
with:
go-version: '1.17'
go-version: "1.19"
- name: Run coverage
run: go test -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Codecov
Expand Down
32 changes: 25 additions & 7 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"reflect"
"regexp"
"strings"
"time"

Expand All @@ -12,7 +13,8 @@ import (
)

var (
_timeType = reflect.TypeOf(time.Time{})
_timeType = reflect.TypeOf(time.Time{})
_genericTypeRegex = regexp.MustCompile(`(.+)\[(.+)\]`)
)

type Schema struct {
Expand Down Expand Up @@ -161,25 +163,40 @@ func typeName(t reflect.Type) string {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t.String()

typeName := t.String()

// When using generic structures reflect's type name looks something like
// module.genericStruct[full/module/path/module.subType]
// OpenAPI does not support "[]" or "/", so we replace this by module.genericStruct..module.subType
match := _genericTypeRegex.FindStringSubmatch(typeName)
if match == nil {
return typeName
} else {
return fmt.Sprintf("%s..%s", match[1], pkgName(match[2]))
}
}

func structReference(t reflect.Type) string {
return fmt.Sprintf("#/components/schemas/%s", typeName(t))
}

func pkgName(t reflect.Type) string {
parts := strings.Split(t.PkgPath(), "/")
func pkgName(p string) string {
parts := strings.Split(p, "/")

return parts[len(parts)-1]
}

func typePkgName(t reflect.Type) string {
return pkgName(t.PkgPath())
}

func (s *Schema) generateStructureSchema(ctx context.Context, doc *openapi3.T, t reflect.Type, inlineLevel int, fieldInfo shared.AttributeInfo, filterObject shared.FilterInterface) (*openapi3.Schema, error) {
ret := &openapi3.Schema{
Type: "object",
}

pkgName := shared.ToSnakeCase(pkgName(t))
pkgName := shared.ToSnakeCase(typePkgName(t))
structName := shared.ToSnakeCase(t.Name())

fieldInfo = fieldInfo.AppendPath(structName)
Expand All @@ -197,6 +214,7 @@ func (s *Schema) generateStructureSchema(ctx context.Context, doc *openapi3.T, t

for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fTypeName := typeName(f.Type)
tag := ParseJsonTag(f)

if (tag.Ignored != nil) && *tag.Ignored {
Expand All @@ -218,8 +236,8 @@ func (s *Schema) generateStructureSchema(ctx context.Context, doc *openapi3.T, t
}

//Detect if field is anonymous, look into the schemas and use the same property
if f.Anonymous && fieldSchema.Ref != "" && doc.Components.Schemas[f.Type.String()] != nil && doc.Components.Schemas[f.Type.String()].Value != nil {
for name, property := range doc.Components.Schemas[f.Type.String()].Value.Properties {
if f.Anonymous && fieldSchema.Ref != "" && doc.Components.Schemas[fTypeName] != nil && doc.Components.Schemas[fTypeName].Value != nil {
for name, property := range doc.Components.Schemas[fTypeName].Value.Properties {
ret.WithPropertyRef(name, property)
}

Expand Down
43 changes: 43 additions & 0 deletions schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ type RecursiveUser struct {
Group *RecursiveGroup
}

type Generic[T any] struct {
Name string
Value T
}

type Embedded struct {
}

func checkGeneratedType(g *goblin.G, ctx context.Context, schemaPtr **Schema, docPtr **openapi3.T, value interface{}, expected string) {
g.It(fmt.Sprintf("should generate inline type for %T", value), func() {
s := *schemaPtr
Expand Down Expand Up @@ -311,6 +319,41 @@ func TestSchema(t *testing.T) {
"type": "string",
"format": "date-time"
}`)

g.It("should generate valid name for generic structures", func() {
st := Generic[Embedded]{
Name: "john",
}
typ := reflect.TypeOf(&st)
schema, err := s.GenerateSchemaFor(ctx, doc, typ)
require.NoError(g, err)

data, err := json.Marshal(schema)
require.NoError(g, err)
// check returned schema
assert.JSONEq(g, `{
"$ref": "#/components/schemas/schema.Generic..schema.Embedded"
}`, string(data))

userSchema, found := doc.Components.Schemas[typeName(typ.Elem())]
require.True(g, found)

data, err = json.Marshal(userSchema)
require.NoError(g, err)

// check returned schema
assert.JSONEq(g, `{
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Value": {
"$ref": "#/components/schemas/schema.Embedded"
}
}
}`, string(data))
})
})

})
Expand Down

0 comments on commit b553cf8

Please sign in to comment.