Skip to content

Commit

Permalink
feat: adds basic generics support
Browse files Browse the repository at this point in the history
  • Loading branch information
kirillDanshin committed May 1, 2022
1 parent 36ae7af commit 65ab05e
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 1 deletion.
9 changes: 9 additions & 0 deletions operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,15 @@ func (operation *Operation) ParseResponseComment(commentLine string, astFile *as
return err
}

if strings.HasSuffix(matches[3], ",") && strings.Contains(matches[3], "[") {
// regexp may have broken generics syntax. find closing bracket and add it back
allMatchesLenOffset := strings.Index(commentLine, matches[3]) + len(matches[3])
lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]")
if lostPartEndIdx >= 0 {
matches[3] += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1]
}
}

description := strings.Trim(matches[4], "\"")

schema, err := operation.parseAPIObjectSchema(strings.Trim(matches[2], "{}"), matches[3], astFile)
Expand Down
107 changes: 106 additions & 1 deletion packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag
}

fullName := typeSpecDef.FullName()
if typeSpecDef.TypeSpec.TypeParams != nil {
fullName = fullName + "["
for i, typeParam := range typeSpecDef.TypeSpec.TypeParams.List {
if i > 0 {
fullName = fullName + ","
}

fullName = fullName + typeParam.Names[0].Name
}
fullName = fullName + "]"
}
anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName]
if ok {
if typeSpecDef.PkgPath == anotherTypeDef.PkgPath {
Expand Down Expand Up @@ -286,7 +297,7 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File
return pkgDefs.uniqueDefinitions[typeName]
}

parts := strings.Split(typeName, ".")
parts := strings.Split(strings.Split(typeName, "[")[0], ".")
if len(parts) > 1 {
isAliasPkgName := func(file *ast.File, pkgName string) bool {
if file != nil && file.Imports != nil {
Expand Down Expand Up @@ -322,6 +333,22 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File
}
}

if strings.Contains(typeName, "[") {
// joinedParts differs from typeName in that it does not contain any type parameters
joinedParts := strings.Join(parts, ".")
for tName, tSpec := range pkgDefs.uniqueDefinitions {
if !strings.Contains(tName, "[") {
continue
}

if strings.Contains(tName, joinedParts) {
if parametrized := pkgDefs.parametrizeStruct(tSpec, typeName); parametrized != nil {
return parametrized
}
}
}
}

return pkgDefs.findTypeSpec(pkgPath, parts[1])
}

Expand All @@ -346,3 +373,81 @@ func (pkgDefs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File

return nil
}

func (pkgDefs *PackagesDefinitions) parametrizeStruct(original *TypeSpecDef, fullGenericForm string) *TypeSpecDef {
genericParams := strings.Split(strings.TrimRight(fullGenericForm, "]"), "[")
if len(genericParams) == 1 {
return nil
}
genericParams = strings.Split(genericParams[1], ",")
for i, p := range genericParams {
genericParams[i] = strings.TrimSpace(p)
}
genericParamTypeDefs := map[string]*TypeSpecDef{}

if len(genericParams) != len(original.TypeSpec.TypeParams.List) {
return nil
}

for i, genericParam := range genericParams {
tdef, ok := pkgDefs.uniqueDefinitions[genericParam]
if !ok {
return nil
}
genericParamTypeDefs[original.TypeSpec.TypeParams.List[i].Names[0].Name] = tdef
}

parametrizedTypeSpec := &TypeSpecDef{
File: original.File,
PkgPath: original.PkgPath,
TypeSpec: &ast.TypeSpec{
Doc: original.TypeSpec.Doc,
Comment: original.TypeSpec.Comment,
Assign: original.TypeSpec.Assign,
},
}

ident := &ast.Ident{
NamePos: original.TypeSpec.Name.NamePos,
Obj: original.TypeSpec.Name.Obj,
}
genNameParts := strings.Split(fullGenericForm, "[")
if strings.Contains(genNameParts[0], ".") {
genNameParts[0] = strings.Split(genNameParts[0], ".")[1]
}
ident.Name = genNameParts[0] + "[" + strings.Replace(strings.Join(genNameParts[1:], ""), ".", "_", -1)
ident.Name = strings.Replace(strings.Replace(ident.Name, "\t", "", -1), " ", "", -1)

parametrizedTypeSpec.TypeSpec.Name = ident

origStructType := original.TypeSpec.Type.(*ast.StructType)

newStructTypeDef := &ast.StructType{
Struct: origStructType.Struct,
Incomplete: origStructType.Incomplete,
Fields: &ast.FieldList{
Opening: origStructType.Fields.Opening,
Closing: origStructType.Fields.Closing,
},
}

for _, field := range origStructType.Fields.List {
newField := &ast.Field{
Doc: field.Doc,
Names: field.Names,
Tag: field.Tag,
Comment: field.Comment,
}
if genTypeSpec, ok := genericParamTypeDefs[field.Type.(*ast.Ident).Name]; ok {
newField.Type = genTypeSpec.TypeSpec.Type
} else {
newField.Type = field.Type
}

newStructTypeDef.Fields.List = append(newStructTypeDef.Fields.List, newField)
}

parametrizedTypeSpec.TypeSpec.Type = newStructTypeDef

return parametrizedTypeSpec
}
197 changes: 197 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,203 @@ func TestParseStructComment(t *testing.T) {
assert.Equal(t, expected, string(b))
}

func TestParseGenericsBasic(t *testing.T) {
t.Parallel()

expected := `{
"swagger": "2.0",
"info": {
"description": "This is a sample server Petstore server.",
"title": "Swagger Example API",
"contact": {},
"version": "1.0"
},
"host": "localhost:4000",
"basePath": "/api",
"paths": {
"/posts/{post_id}": {
"get": {
"description": "get string by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Add a new pet to the store",
"parameters": [
{
"type": "integer",
"format": "int64",
"description": "Some ID",
"name": "post_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/web.GenericResponse[web_Post]"
}
},
"222": {
"description": "",
"schema": {
"$ref": "#/definitions/web.GenericResponseMulti[web_Post,web_Post]"
}
},
"400": {
"description": "We need ID!!",
"schema": {
"$ref": "#/definitions/web.APIError"
}
},
"404": {
"description": "Can not find ID",
"schema": {
"$ref": "#/definitions/web.APIError"
}
}
}
}
}
},
"definitions": {
"web.APIError": {
"description": "API error with information about it",
"type": "object",
"properties": {
"createdAt": {
"description": "Error time",
"type": "string"
},
"error": {
"description": "Error an Api error",
"type": "string"
},
"errorCtx": {
"description": "Error ` + "`context`" + ` tick comment",
"type": "string"
},
"errorNo": {
"description": "Error ` + "`number`" + ` tick comment",
"type": "integer"
}
}
},
"web.GenericResponseMulti[web_Post,web_Post]": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"data": {
"description": "Post data",
"type": "object",
"properties": {
"name": {
"description": "Post tag",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"id": {
"type": "integer",
"format": "int64",
"example": 1
},
"name": {
"description": "Post name",
"type": "string",
"example": "poti"
}
}
},
"meta": {
"type": "object",
"properties": {
"data": {
"description": "Post data",
"type": "object",
"properties": {
"name": {
"description": "Post tag",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"id": {
"type": "integer",
"format": "int64",
"example": 1
},
"name": {
"description": "Post name",
"type": "string",
"example": "poti"
}
}
},
"status": {
"type": "string"
}
}
},
"web.GenericResponse[web_Post]": {
"type": "object",
"properties": {
"data": {
"type": "object",
"properties": {
"data": {
"description": "Post data",
"type": "object",
"properties": {
"name": {
"description": "Post tag",
"type": "array",
"items": {
"type": "string"
}
}
}
},
"id": {
"type": "integer",
"format": "int64",
"example": 1
},
"name": {
"description": "Post name",
"type": "string",
"example": "poti"
}
}
},
"status": {
"type": "string"
}
}
}
}
}`

searchDir := "testdata/generics_basic"
p := New()
err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth)
assert.NoError(t, err)
b, _ := json.MarshalIndent(p.swagger, "", " ")
assert.Equal(t, expected, string(b))
}

func TestParseNonExportedJSONFields(t *testing.T) {
t.Parallel()

Expand Down
19 changes: 19 additions & 0 deletions testdata/generics_basic/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package api

import (
"net/http"
)

// @Summary Add a new pet to the store
// @Description get string by ID
// @Accept json
// @Produce json
// @Param post_id path int true "Some ID" Format(int64)
// @Success 200 {object} web.GenericResponse[web.Post]
// @Success 222 {object} web.GenericResponseMulti[web.Post, web.Post]
// @Failure 400 {object} web.APIError "We need ID!!"
// @Failure 404 {object} web.APIError "Can not find ID"
// @Router /posts/{post_id} [get]
func GetPost(w http.ResponseWriter, r *http.Request) {
//write your code
}
17 changes: 17 additions & 0 deletions testdata/generics_basic/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"net/http"

"github.com/swaggo/swag/testdata/generics_basic/api"
)

// @title Swagger Example API
// @version 1.0
// @description This is a sample server Petstore server.
// @host localhost:4000
// @basePath /api
func main() {
http.HandleFunc("/posts/", api.GetPost)
http.ListenAndServe(":8080", nil)
}
Loading

0 comments on commit 65ab05e

Please sign in to comment.