Skip to content

Commit

Permalink
feat: add function scoped struct parse (#1283)
Browse files Browse the repository at this point in the history
  • Loading branch information
mstrYoda authored Aug 16, 2022
1 parent af1c525 commit 45f01a1
Show file tree
Hide file tree
Showing 12 changed files with 408 additions and 3 deletions.
59 changes: 59 additions & 0 deletions packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (pkgDefs *PackagesDefinitions) ParseTypes() (map[*TypeSpecDef]*Schema, erro
parsedSchemas := make(map[*TypeSpecDef]*Schema)
for astFile, info := range pkgDefs.files {
pkgDefs.parseTypesFromFile(astFile, info.PackagePath, parsedSchemas)
pkgDefs.parseFunctionScopedTypesFromFile(astFile, info.PackagePath, parsedSchemas)
}
return parsedSchemas, nil
}
Expand Down Expand Up @@ -161,6 +162,64 @@ func (pkgDefs *PackagesDefinitions) parseTypesFromFile(astFile *ast.File, packag
}
}

func (pkgDefs *PackagesDefinitions) parseFunctionScopedTypesFromFile(astFile *ast.File, packagePath string, parsedSchemas map[*TypeSpecDef]*Schema) {
for _, astDeclaration := range astFile.Decls {
if funcDeclaration, ok := astDeclaration.(*ast.FuncDecl); ok {
for _, stmt := range funcDeclaration.Body.List {
if declStmt, ok := (stmt).(*ast.DeclStmt); ok {
if genDecl, ok := (declStmt.Decl).(*ast.GenDecl); ok && genDecl.Tok == token.TYPE {
for _, astSpec := range genDecl.Specs {
if typeSpec, ok := astSpec.(*ast.TypeSpec); ok {
typeSpecDef := &TypeSpecDef{
PkgPath: packagePath,
File: astFile,
TypeSpec: typeSpec,
ParentSpec: astDeclaration,
}

if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) && parsedSchemas != nil {
parsedSchemas[typeSpecDef] = &Schema{
PkgPath: typeSpecDef.PkgPath,
Name: astFile.Name.Name,
Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)),
}
}

if pkgDefs.uniqueDefinitions == nil {
pkgDefs.uniqueDefinitions = make(map[string]*TypeSpecDef)
}

fullName := typeSpecFullName(typeSpecDef)

anotherTypeDef, ok := pkgDefs.uniqueDefinitions[fullName]
if ok {
if typeSpecDef.PkgPath == anotherTypeDef.PkgPath {
continue
} else {
delete(pkgDefs.uniqueDefinitions, fullName)
}
} else {
pkgDefs.uniqueDefinitions[fullName] = typeSpecDef
}

if pkgDefs.packages[typeSpecDef.PkgPath] == nil {
pkgDefs.packages[typeSpecDef.PkgPath] = &PackageDefinitions{
Name: astFile.Name.Name,
TypeDefinitions: map[string]*TypeSpecDef{fullName: typeSpecDef},
}
} else if _, ok = pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName]; !ok {
pkgDefs.packages[typeSpecDef.PkgPath].TypeDefinitions[fullName] = typeSpecDef
}
}
}

}
}
}
}
}
}

func (pkgDefs *PackagesDefinitions) findTypeSpec(pkgPath string, typeName string) *TypeSpecDef {
if pkgDefs.packages == nil {
return nil
Expand Down
45 changes: 45 additions & 0 deletions packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,51 @@ func TestPackagesDefinitions_ParseTypes(t *testing.T) {
assert.NoError(t, err)
}

func TestPackagesDefinitions_parseFunctionScopedTypesFromFile(t *testing.T) {
mainAST := &ast.File{
Name: &ast.Ident{Name: "main.go"},
Decls: []ast.Decl{
&ast.FuncDecl{
Name: ast.NewIdent("TestFuncDecl"),
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.DeclStmt{
Decl: &ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{
&ast.TypeSpec{
Name: ast.NewIdent("response"),
Type: ast.NewIdent("struct"),
},
&ast.TypeSpec{
Name: ast.NewIdent("stringResponse"),
Type: ast.NewIdent("string"),
},
},
},
},
},
},
},
},
}

pd := PackagesDefinitions{
packages: make(map[string]*PackageDefinitions),
}

parsedSchema := make(map[*TypeSpecDef]*Schema)
pd.parseFunctionScopedTypesFromFile(mainAST, "main", parsedSchema)

assert.Len(t, parsedSchema, 1)

_, ok := pd.uniqueDefinitions["main.go.TestFuncDecl.response"]
assert.True(t, ok)

_, ok = pd.packages["main"].TypeDefinitions["main.go.TestFuncDecl.response"]
assert.True(t, ok)
}

func TestPackagesDefinitions_FindTypeSpec(t *testing.T) {
userDef := TypeSpecDef{
File: &ast.File{
Expand Down
15 changes: 14 additions & 1 deletion parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,12 @@ func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool {
// with a schema for the given type
func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) {
typeName := typeSpecDef.FullName()
refTypeName := TypeDocName(typeName, typeSpecDef.TypeSpec)
var refTypeName string
if fn, ok := (typeSpecDef.ParentSpec).(*ast.FuncDecl); ok {
refTypeName = TypeDocNameFuncScoped(typeName, typeSpecDef.TypeSpec, fn.Name.Name)
} else {
refTypeName = TypeDocName(typeName, typeSpecDef.TypeSpec)
}

schema, found := parser.parsedSchemas[typeSpecDef]
if found {
Expand Down Expand Up @@ -1073,6 +1078,14 @@ func fullTypeName(pkgName, typeName string) string {
return typeName
}

func fullTypeNameFunctionScoped(pkgName, fnName, typeName string) string {
if pkgName != "" {
return pkgName + "." + fnName + "." + typeName
}

return typeName
}

// fillDefinitionDescription additionally fills fields in definition (spec.Schema)
// TODO: If .go file contains many types, it may work for a long time
func fillDefinitionDescription(definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) {
Expand Down
156 changes: 156 additions & 0 deletions parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ func TestParser_ParseDefinition(t *testing.T) {
}
_, err = p.ParseDefinition(definition)
assert.Error(t, err)

// Parsing *ast.FuncType with parent spec
definition = &TypeSpecDef{
PkgPath: "github.com/swagger/swag/model",
File: &ast.File{
Name: &ast.Ident{
Name: "model",
},
},
TypeSpec: &ast.TypeSpec{
Name: &ast.Ident{
Name: "Test",
},
Type: &ast.FuncType{},
},
ParentSpec: &ast.FuncDecl{
Name: ast.NewIdent("TestFuncDecl"),
},
}
_, err = p.ParseDefinition(definition)
assert.Error(t, err)
assert.Equal(t, "model.TestFuncDecl.Test", definition.FullName())
}

func TestParser_ParseGeneralApiInfo(t *testing.T) {
Expand Down Expand Up @@ -2167,6 +2189,16 @@ func TestParseDuplicatedOtherMethods(t *testing.T) {
assert.Errorf(t, err, "duplicated @id declarations successfully found")
}

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

searchDir := "testdata/duplicated_function_scoped"
p := New()
p.ParseDependency = true
err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth)
assert.Errorf(t, err, "duplicated @id declarations successfully found")
}

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

Expand Down Expand Up @@ -3233,6 +3265,130 @@ func Fun() {
assert.Equal(t, "#/definitions/Teacher", ref.String())
}

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

src := `
package main
// @Param request body main.Fun.request true "query params"
// @Success 200 {object} main.Fun.response
// @Router /test [post]
func Fun() {
type request struct {
Name string
}
type response struct {
Name string
Child string
}
}
`
f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments)
assert.NoError(t, err)

p := New()
_ = p.packages.CollectAstFile("api", "api/api.go", f)
_, err = p.packages.ParseTypes()
assert.NoError(t, err)

err = p.ParseRouterAPIInfo("", f)
assert.NoError(t, err)

_, ok := p.swagger.Definitions["main.Fun.response"]
assert.True(t, ok)
}

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

src := `
package main
// @Param request body main.Fun.request true "query params"
// @Success 200 {object} main.Fun.response
// @Router /test [post]
func Fun() {
type request struct {
Name string
}
type response struct {
Name string
Child string
}
}
`
expected := `{
"info": {
"contact": {}
},
"paths": {
"/test": {
"post": {
"parameters": [
{
"description": "query params",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/main.Fun.request"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.Fun.response"
}
}
}
}
}
},
"definitions": {
"main.Fun.request": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
}
},
"main.Fun.response": {
"type": "object",
"properties": {
"child": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
}
}`

f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments)
assert.NoError(t, err)

p := New()
_ = p.packages.CollectAstFile("api", "api/api.go", f)

_, err = p.packages.ParseTypes()
assert.NoError(t, err)

err = p.ParseRouterAPIInfo("", f)
assert.NoError(t, err)

b, _ := json.MarshalIndent(p.swagger, "", " ")
t.Log(string(b))
assert.Equal(t, expected, string(b))
}

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

Expand Down
24 changes: 24 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,30 @@ func ignoreNameOverride(name string) bool {
return len(name) != 0 && name[0] == IgnoreNameOverridePrefix
}

// TypeDocNameFuncScoped get alias from comment '// @name ', otherwise the original type name to display in doc.
func TypeDocNameFuncScoped(pkgName string, spec *ast.TypeSpec, fnName string) string {
if spec != nil && !ignoreNameOverride(pkgName) {
if spec.Comment != nil {
for _, comment := range spec.Comment.List {
texts := strings.Split(strings.TrimSpace(strings.TrimLeft(comment.Text, "/")), " ")
if len(texts) > 1 && strings.ToLower(texts[0]) == "@name" {
return texts[1]
}
}
}

if spec.Name != nil {
return fullTypeNameFunctionScoped(strings.Split(pkgName, ".")[0], fnName, spec.Name.Name)
}
}

if ignoreNameOverride(pkgName) {
return pkgName[1:]
}

return pkgName
}

// RefSchema build a reference schema.
func RefSchema(refType string) *spec.Schema {
return spec.RefSchema("#/definitions/" + refType)
Expand Down
27 changes: 27 additions & 0 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,30 @@ func TestTypeDocName(t *testing.T) {
},
}))
}

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

expected := "a/package"
assert.Equal(t, expected, TypeDocNameFuncScoped(expected, nil, "FnName"))

expected = "package.FnName.Model"
assert.Equal(t, expected, TypeDocNameFuncScoped("package", &ast.TypeSpec{Name: &ast.Ident{Name: "Model"}}, "FnName"))

expected = "Model"
assert.Equal(t, expected, TypeDocNameFuncScoped("package", &ast.TypeSpec{
Comment: &ast.CommentGroup{
List: []*ast.Comment{{Text: "// @name Model"}},
},
}, "FnName"))

expected = "package.FnName.ModelName"
assert.Equal(t, expected, TypeDocNameFuncScoped("$package.FnName.ModelName", &ast.TypeSpec{Name: &ast.Ident{Name: "Model"}}, "FnName"))

expected = "Model"
assert.Equal(t, expected, TypeDocNameFuncScoped("$Model", &ast.TypeSpec{
Comment: &ast.CommentGroup{
List: []*ast.Comment{{Text: "// @name ModelName"}},
},
}, "FnName"))
}
Loading

0 comments on commit 45f01a1

Please sign in to comment.