Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docparse/docparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ var (
)

// parseComment a single comment block in the file filePath.
func parseComment(prog *Program, comment, pkgPath, filePath string) ([]*Endpoint, int, error) {
func parseComment(prog *Program, comment, _, filePath string) ([]*Endpoint, int, error) {
e := &Endpoint{}

// Get start line and determine if this is a comment block.
Expand Down
2 changes: 1 addition & 1 deletion docparse/docparse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ func TestParseResponse(t *testing.T) {
t.Errorf("wrong code\nwant: %v\ngot: %v", tt.wantCode, code)
}
if d := diff.Diff(tt.wantResp, resp); d != "" {
t.Errorf(d)
t.Error(d)
}
})
}
Expand Down
125 changes: 117 additions & 8 deletions docparse/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func structToSchema(prog *Program, name, tagName string, ref Reference) (*Schema
name = p.Name
}

prop, err := fieldToSchema(prog, name, tagName, ref, p.KindField)
prop, err := fieldToSchema(prog, name, tagName, ref, p.KindField, nil)
if err != nil {
return nil, fmt.Errorf("cannot parse %v: %v", ref.Lookup, err)
}
Expand Down Expand Up @@ -215,7 +215,13 @@ func setTags(name, fName string, p *Schema, tags []string) error {
}

// Convert a struct field to JSON schema.
func fieldToSchema(prog *Program, fName, tagName string, ref Reference, f *ast.Field) (*Schema, error) {
func fieldToSchema(
prog *Program,
fName, tagName string,
ref Reference,
f *ast.Field,
generics map[string]string,
) (*Schema, error) {
var p Schema

if f.Doc != nil {
Expand Down Expand Up @@ -283,6 +289,10 @@ start:
if mappedType != "" {
p.Type = JSONSchemaType(mappedType)
}
if generics != nil && generics[typ.Name] != "" {
mappedType = "generics"
p.Type = JSONSchemaType(generics[typ.Name])
}
if p.Type == "enum" && len(p.Enum) == 0 {
if variations, err := getEnumVariations(ref.File, pkg, typ.Name); len(variations) > 0 {
p.Enum = variations
Expand Down Expand Up @@ -320,7 +330,7 @@ start:
p.Properties = map[string]*Schema{}
for _, f := range typ.Fields.List {
propName := goutil.TagName(f, tagName)
prop, err := fieldToSchema(prog, propName, tagName, ref, f)
prop, err := fieldToSchema(prog, propName, tagName, ref, f, generics)
if err != nil {
return nil, fmt.Errorf("anon struct: %v", err)
}
Expand Down Expand Up @@ -373,7 +383,7 @@ start:
case *ast.ArrayType:
isEnum := p.Type == "enum"
p.Type = "array"
err := resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum)
err := resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum, generics)
if err != nil {
return nil, err
}
Expand All @@ -393,6 +403,9 @@ start:
dbg("ERR FOUND MapType: %s", err.Error())
return &p, nil
}
if generics != nil && generics[vtyp.Name] != "" {
vtyp.Name = generics[vtyp.Name]
}
if isPrimitive(vtyp.Name) {
// we are done, no need for a lookup of a custom type
p.AdditionalProperties = &Schema{Type: JSONSchemaType(vtyp.Name)}
Expand All @@ -418,13 +431,34 @@ start:
isEnum := p.Type == "enum"
p.Type = "array"

err := resolveArray(prog, ref, pkg, &p, typ.Elt, isEnum)
err := resolveArray(prog, ref, pkg, &p, typ.Elt, isEnum, generics)
if err != nil {
return nil, err
}

return &p, nil

// Generic types
case *ast.IndexExpr:
genericsIdent, ok := typ.X.(*ast.Ident)
if !ok {
return nil, fmt.Errorf("unknown generic type: %T", typ.X)
}
if err := fillGenericsSchema(prog, &p, tagName, ref, genericsIdent, generics, typ.Index); err != nil {
return nil, fmt.Errorf("generic fieldToSchema: %v", err)
}
return &p, nil

case *ast.IndexListExpr:
genericsIdent, ok := typ.X.(*ast.Ident)
if !ok {
return nil, fmt.Errorf("unknown generic type: %T", typ.X)
}
if err := fillGenericsSchema(prog, &p, tagName, ref, genericsIdent, generics, typ.Indices...); err != nil {
return nil, fmt.Errorf("generic fieldToSchema: %v", err)
}
return &p, nil

default:
return nil, fmt.Errorf("fieldToSchema: unknown type: %T", typ)
}
Expand Down Expand Up @@ -456,6 +490,69 @@ start:
return &p, nil
}

// fillGenericsSchema fills the schema with the generic type information. As the
// types can be different for every generics declaration they will need to be a
// anonymos object in the schema output instead of a reusable reference.
func fillGenericsSchema(
prog *Program,
p *Schema,
tagName string,
ref Reference,
genericsIdent *ast.Ident,
generics map[string]string,
indices ...ast.Expr,
) error {
genericsType, _, _, err := findType(ref.File, ref.Package, genericsIdent.Name)
if err != nil {
return fmt.Errorf("cannot find generic type: %v", err)
}

var genericsTemplateIDs []string
for _, item := range genericsType.TypeParams.List {
for _, name := range item.Names {
genericsTemplateIDs = append(genericsTemplateIDs, name.Name)
}
}

if generics == nil {
generics = make(map[string]string)
}
if len(genericsTemplateIDs) > 0 {
if len(indices) != len(genericsTemplateIDs) {
return fmt.Errorf("generic type has %d template IDs, but %d arguments were provided",
len(genericsTemplateIDs), len(indices))
}
for i := 0; i < len(indices); i++ {
arg, _, err := findTypeIdent(indices[i], ref.Package)
if err != nil {
return fmt.Errorf("cannot find generic type argument: %v", err)
}
generics[genericsTemplateIDs[i]] = arg.Name
}
}

genericsStruct, ok := genericsType.Type.(*ast.StructType)
if !ok {
return fmt.Errorf("generic type is not a struct: %T", genericsType.Type)
}

p.Type = "object"
if p.Properties == nil {
p.Properties = make(map[string]*Schema)
}

for _, field := range genericsStruct.Fields.List {
fieldName := goutil.TagName(field, tagName)
schema, err := fieldToSchema(prog, fieldName, tagName, ref, field, generics)
if err != nil {
return fmt.Errorf("generic fieldToSchema: %v", err)
}
p.Properties[fieldName] = schema
}

return nil
}

// Helper function to extract enum variations from a file.
func getEnumVariations(currentFile, pkgPath, typeName string) ([]string, error) {
resolvedPath, pkg, err := resolvePackage(currentFile, pkgPath)
Expand Down Expand Up @@ -532,7 +629,15 @@ func lookupTypeAndRef(file, pkg, name string) (string, string, error) {
return t, sRef, nil
}

func resolveArray(prog *Program, ref Reference, pkg string, p *Schema, typ ast.Expr, isEnum bool) error {
func resolveArray(
prog *Program,
ref Reference,
pkg string,
p *Schema,
typ ast.Expr,
isEnum bool,
generics map[string]string,
) error {
asw := typ

var name *ast.Ident
Expand All @@ -550,7 +655,11 @@ arrayStart:

dbg("resolveArray: ident: %#v in %#v", typ.Name, pkg)

p.Items = &Schema{Type: JSONSchemaType(typ.Name)}
if generics != nil && generics[typ.Name] != "" {
p.Items = &Schema{Type: JSONSchemaType(generics[typ.Name])}
} else {
p.Items = &Schema{Type: JSONSchemaType(typ.Name)}
}

// Generally an item is an enum rather than the array itself
if len(p.Enum) > 0 {
Expand Down Expand Up @@ -671,7 +780,7 @@ func JSONSchemaType(t string) string {
return t
}

func getTypeInfo(prog *Program, lookup, filePath string) (string, error) {
func getTypeInfo(_ *Program, lookup, filePath string) (string, error) {
// TODO: REMOVE THE prog PARAM, as this function is not
// using it anymore.
dbg("getTypeInfo: %#v in %#v", lookup, filePath)
Expand Down
4 changes: 2 additions & 2 deletions docparse/jsonschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestFieldToProperty(t *testing.T) {
Package: "a",
File: "./testdata/src/a/a.go",
Context: "req",
}, f)
}, f, nil)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -91,7 +91,7 @@ func TestFieldToProperty(t *testing.T) {
out, err := fieldToSchema(prog, f.Names[0].Name, "json", Reference{
Package: "a",
File: "./testdata/src/a/a.go",
}, f)
}, f, nil)
if err != nil {
t.Fatal(err)
}
Expand Down
26 changes: 26 additions & 0 deletions testdata/openapi2/src/generics/in.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package generics

type myGeneric[T, N any] struct {
// This is a simple field.
Field1 T
// This is a array field.
Field2 []T
// This is a map field.
Field3 map[string]T
// This is another simple field.
Field4 int
// This is a different tag field.
Field5 N `json:"hello5"`
// This is a different tag field with pointer.
Field6 *N `json:"hello6"`
}

type reqRef struct {
// Foo documents a generic type.
Foo myGeneric[string, float64]
}

// POST /path
//
// Request body: reqRef
// Response 200: {empty}
56 changes: 56 additions & 0 deletions testdata/openapi2/src/generics/want.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
swagger: "2.0"
info:
title: x
version: x
consumes:
- application/json
produces:
- application/json
paths:
/path:
post:
operationId: POST_path
consumes:
- application/json
produces:
- application/json
parameters:
- name: generics.reqRef
in: body
required: true
schema:
$ref: '#/definitions/generics.reqRef'
responses:
200:
description: 200 OK (no data)
definitions:
generics.reqRef:
title: reqRef
type: object
properties:
Foo:
description: Foo documents a generic type.
type: object
properties:
Field1:
description: This is a simple field.
type: string
Field2:
description: This is a array field.
type: array
items:
type: string
Field3:
description: This is a map field.
type: object
additionalProperties:
type: string
Field4:
description: This is another simple field.
type: integer
hello5:
description: This is a different tag field.
type: number
hello6:
description: This is a different tag field with pointer.
type: number
Loading