Skip to content

Commit 005d770

Browse files
committed
Feature: Add support for generics
Allow generating documentation for generic types.
1 parent da5d378 commit 005d770

6 files changed

Lines changed: 203 additions & 12 deletions

File tree

docparse/docparse.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ var (
177177
)
178178

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

183183
// Get start line and determine if this is a comment block.

docparse/docparse_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ func TestParseResponse(t *testing.T) {
585585
t.Errorf("wrong code\nwant: %v\ngot: %v", tt.wantCode, code)
586586
}
587587
if d := diff.Diff(tt.wantResp, resp); d != "" {
588-
t.Errorf(d)
588+
t.Error(d)
589589
}
590590
})
591591
}

docparse/jsonschema.go

Lines changed: 117 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func structToSchema(prog *Program, name, tagName string, ref Reference) (*Schema
7070
name = p.Name
7171
}
7272

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

217217
// Convert a struct field to JSON schema.
218-
func fieldToSchema(prog *Program, fName, tagName string, ref Reference, f *ast.Field) (*Schema, error) {
218+
func fieldToSchema(
219+
prog *Program,
220+
fName, tagName string,
221+
ref Reference,
222+
f *ast.Field,
223+
generics map[string]string,
224+
) (*Schema, error) {
219225
var p Schema
220226

221227
if f.Doc != nil {
@@ -283,6 +289,10 @@ start:
283289
if mappedType != "" {
284290
p.Type = JSONSchemaType(mappedType)
285291
}
292+
if generics != nil && generics[typ.Name] != "" {
293+
mappedType = "generics"
294+
p.Type = JSONSchemaType(generics[typ.Name])
295+
}
286296
if p.Type == "enum" && len(p.Enum) == 0 {
287297
if variations, err := getEnumVariations(ref.File, pkg, typ.Name); len(variations) > 0 {
288298
p.Enum = variations
@@ -320,7 +330,7 @@ start:
320330
p.Properties = map[string]*Schema{}
321331
for _, f := range typ.Fields.List {
322332
propName := goutil.TagName(f, tagName)
323-
prop, err := fieldToSchema(prog, propName, tagName, ref, f)
333+
prop, err := fieldToSchema(prog, propName, tagName, ref, f, generics)
324334
if err != nil {
325335
return nil, fmt.Errorf("anon struct: %v", err)
326336
}
@@ -373,7 +383,7 @@ start:
373383
case *ast.ArrayType:
374384
isEnum := p.Type == "enum"
375385
p.Type = "array"
376-
err := resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum)
386+
err := resolveArray(prog, ref, pkg, &p, resolvType.Elt, isEnum, generics)
377387
if err != nil {
378388
return nil, err
379389
}
@@ -393,6 +403,9 @@ start:
393403
dbg("ERR FOUND MapType: %s", err.Error())
394404
return &p, nil
395405
}
406+
if generics != nil && generics[vtyp.Name] != "" {
407+
vtyp.Name = generics[vtyp.Name]
408+
}
396409
if isPrimitive(vtyp.Name) {
397410
// we are done, no need for a lookup of a custom type
398411
p.AdditionalProperties = &Schema{Type: JSONSchemaType(vtyp.Name)}
@@ -418,13 +431,34 @@ start:
418431
isEnum := p.Type == "enum"
419432
p.Type = "array"
420433

421-
err := resolveArray(prog, ref, pkg, &p, typ.Elt, isEnum)
434+
err := resolveArray(prog, ref, pkg, &p, typ.Elt, isEnum, generics)
422435
if err != nil {
423436
return nil, err
424437
}
425438

426439
return &p, nil
427440

441+
// Generic types
442+
case *ast.IndexExpr:
443+
genericsIdent, ok := typ.X.(*ast.Ident)
444+
if !ok {
445+
return nil, fmt.Errorf("unknown generic type: %T", typ.X)
446+
}
447+
if err := fillGenericsSchema(prog, &p, tagName, ref, genericsIdent, generics, typ.Index); err != nil {
448+
return nil, fmt.Errorf("generic fieldToSchema: %v", err)
449+
}
450+
return &p, nil
451+
452+
case *ast.IndexListExpr:
453+
genericsIdent, ok := typ.X.(*ast.Ident)
454+
if !ok {
455+
return nil, fmt.Errorf("unknown generic type: %T", typ.X)
456+
}
457+
if err := fillGenericsSchema(prog, &p, tagName, ref, genericsIdent, generics, typ.Indices...); err != nil {
458+
return nil, fmt.Errorf("generic fieldToSchema: %v", err)
459+
}
460+
return &p, nil
461+
428462
default:
429463
return nil, fmt.Errorf("fieldToSchema: unknown type: %T", typ)
430464
}
@@ -456,6 +490,69 @@ start:
456490
return &p, nil
457491
}
458492

493+
// fillGenericsSchema fills the schema with the generic type information. As the
494+
// types can be different for every generics declaration they will need to be a
495+
// anonymos object in the schema output instead of a reusable reference.
496+
func fillGenericsSchema(
497+
prog *Program,
498+
p *Schema,
499+
tagName string,
500+
ref Reference,
501+
genericsIdent *ast.Ident,
502+
generics map[string]string,
503+
indices ...ast.Expr,
504+
) error {
505+
genericsType, _, _, err := findType(ref.File, ref.Package, genericsIdent.Name)
506+
if err != nil {
507+
return fmt.Errorf("cannot find generic type: %v", err)
508+
}
509+
510+
var genericsTemplateIDs []string
511+
for _, item := range genericsType.TypeParams.List {
512+
for _, name := range item.Names {
513+
genericsTemplateIDs = append(genericsTemplateIDs, name.Name)
514+
}
515+
}
516+
517+
if generics == nil {
518+
generics = make(map[string]string)
519+
}
520+
if len(genericsTemplateIDs) > 0 {
521+
if len(indices) != len(genericsTemplateIDs) {
522+
return fmt.Errorf("generic type has %d template IDs, but %d arguments were provided",
523+
len(genericsTemplateIDs), len(indices))
524+
}
525+
for i := 0; i < len(indices); i++ {
526+
arg, _, err := findTypeIdent(indices[i], ref.Package)
527+
if err != nil {
528+
return fmt.Errorf("cannot find generic type argument: %v", err)
529+
}
530+
generics[genericsTemplateIDs[i]] = arg.Name
531+
}
532+
}
533+
534+
genericsStruct, ok := genericsType.Type.(*ast.StructType)
535+
if !ok {
536+
return fmt.Errorf("generic type is not a struct: %T", genericsType.Type)
537+
}
538+
539+
p.Type = "object"
540+
if p.Properties == nil {
541+
p.Properties = make(map[string]*Schema)
542+
}
543+
544+
for _, field := range genericsStruct.Fields.List {
545+
fieldName := goutil.TagName(field, tagName)
546+
schema, err := fieldToSchema(prog, fieldName, tagName, ref, field, generics)
547+
if err != nil {
548+
return fmt.Errorf("generic fieldToSchema: %v", err)
549+
}
550+
p.Properties[fieldName] = schema
551+
}
552+
553+
return nil
554+
}
555+
459556
// Helper function to extract enum variations from a file.
460557
func getEnumVariations(currentFile, pkgPath, typeName string) ([]string, error) {
461558
resolvedPath, pkg, err := resolvePackage(currentFile, pkgPath)
@@ -532,7 +629,15 @@ func lookupTypeAndRef(file, pkg, name string) (string, string, error) {
532629
return t, sRef, nil
533630
}
534631

535-
func resolveArray(prog *Program, ref Reference, pkg string, p *Schema, typ ast.Expr, isEnum bool) error {
632+
func resolveArray(
633+
prog *Program,
634+
ref Reference,
635+
pkg string,
636+
p *Schema,
637+
typ ast.Expr,
638+
isEnum bool,
639+
generics map[string]string,
640+
) error {
536641
asw := typ
537642

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

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

553-
p.Items = &Schema{Type: JSONSchemaType(typ.Name)}
658+
if generics != nil && generics[typ.Name] != "" {
659+
p.Items = &Schema{Type: JSONSchemaType(generics[typ.Name])}
660+
} else {
661+
p.Items = &Schema{Type: JSONSchemaType(typ.Name)}
662+
}
554663

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

674-
func getTypeInfo(prog *Program, lookup, filePath string) (string, error) {
783+
func getTypeInfo(_ *Program, lookup, filePath string) (string, error) {
675784
// TODO: REMOVE THE prog PARAM, as this function is not
676785
// using it anymore.
677786
dbg("getTypeInfo: %#v in %#v", lookup, filePath)

docparse/jsonschema_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func TestFieldToProperty(t *testing.T) {
5555
Package: "a",
5656
File: "./testdata/src/a/a.go",
5757
Context: "req",
58-
}, f)
58+
}, f, nil)
5959
if err != nil {
6060
t.Fatal(err)
6161
}
@@ -91,7 +91,7 @@ func TestFieldToProperty(t *testing.T) {
9191
out, err := fieldToSchema(prog, f.Names[0].Name, "json", Reference{
9292
Package: "a",
9393
File: "./testdata/src/a/a.go",
94-
}, f)
94+
}, f, nil)
9595
if err != nil {
9696
t.Fatal(err)
9797
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package generics
2+
3+
type myGeneric[T, N any] struct {
4+
// This is a simple field.
5+
Field1 T
6+
// This is a array field.
7+
Field2 []T
8+
// This is a map field.
9+
Field3 map[string]T
10+
// This is another simple field.
11+
Field4 int
12+
// This is a different tag field.
13+
Field5 N `json:"hello5"`
14+
// This is a different tag field with pointer.
15+
Field6 *N `json:"hello6"`
16+
}
17+
18+
type reqRef struct {
19+
// Foo documents a generic type.
20+
Foo myGeneric[string, float64]
21+
}
22+
23+
// POST /path
24+
//
25+
// Request body: reqRef
26+
// Response 200: {empty}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
swagger: "2.0"
2+
info:
3+
title: x
4+
version: x
5+
consumes:
6+
- application/json
7+
produces:
8+
- application/json
9+
paths:
10+
/path:
11+
post:
12+
operationId: POST_path
13+
consumes:
14+
- application/json
15+
produces:
16+
- application/json
17+
parameters:
18+
- name: generics.reqRef
19+
in: body
20+
required: true
21+
schema:
22+
$ref: '#/definitions/generics.reqRef'
23+
responses:
24+
200:
25+
description: 200 OK (no data)
26+
definitions:
27+
generics.reqRef:
28+
title: reqRef
29+
type: object
30+
properties:
31+
Foo:
32+
description: Foo documents a generic type.
33+
type: object
34+
properties:
35+
Field1:
36+
description: This is a simple field.
37+
type: string
38+
Field2:
39+
description: This is a array field.
40+
type: array
41+
items:
42+
type: string
43+
Field3:
44+
description: This is a map field.
45+
type: object
46+
additionalProperties:
47+
type: string
48+
Field4:
49+
description: This is another simple field.
50+
type: integer
51+
hello5:
52+
description: This is a different tag field.
53+
type: number
54+
hello6:
55+
description: This is a different tag field with pointer.
56+
type: number

0 commit comments

Comments
 (0)