From c5fb1a1794cb752f4ea35f841dc14242747b267f Mon Sep 17 00:00:00 2001 From: sdghchj Date: Sun, 14 Jun 2020 13:37:18 +0800 Subject: [PATCH] refactor: fix more than one models have the same the from different packages (#736) --- go.mod | 1 + operation.go | 67 +- operation_test.go | 224 ++--- packages.go | 234 +++++ parser.go | 837 +++++++---------- parser_test.go | 901 +------------------ property.go | 139 --- property_test.go | 322 ------- schema.go | 31 + testdata/alias_import/expected.json | 1 - testdata/model_not_under_root/cmd/api/api.go | 34 - testdata/model_not_under_root/cmd/main.go | 53 -- testdata/model_not_under_root/data/foo.go | 5 - testdata/nested/expected.json | 2 - testdata/simple/api/api.go | 11 + testdata/simple/expected.json | 727 +++++++++++++++ types.go | 59 ++ 17 files changed, 1503 insertions(+), 2145 deletions(-) create mode 100644 packages.go delete mode 100644 property.go delete mode 100644 property_test.go delete mode 100644 testdata/model_not_under_root/cmd/api/api.go delete mode 100644 testdata/model_not_under_root/cmd/main.go delete mode 100644 testdata/model_not_under_root/data/foo.go create mode 100644 testdata/simple/expected.json create mode 100644 types.go diff --git a/go.mod b/go.mod index f02834645..de8aa6334 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/go-openapi/jsonreference v0.19.3 github.com/go-openapi/spec v0.19.4 github.com/satori/go.uuid v1.2.0 + github.com/shopspring/decimal v1.2.0 github.com/stretchr/testify v1.4.0 github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 github.com/swaggo/gin-swagger v1.2.0 diff --git a/operation.go b/operation.go index ebdcaf4ba..ba8b68546 100644 --- a/operation.go +++ b/operation.go @@ -46,8 +46,12 @@ var mimeTypePattern = regexp.MustCompile("^[^/]+/[^/]+$") // NewOperation creates a new Operation with default properties. // map[int]Response -func NewOperation() *Operation { +func NewOperation(parser *Parser) *Operation { + if parser == nil { + parser = New() + } return &Operation{ + parser: parser, HTTPMethod: "get", Operation: spec.Operation{ OperationProps: spec.OperationProps{}, @@ -184,16 +188,7 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F }, } case OBJECT: - refType, typeSpec, err := operation.registerSchemaType(refType, astFile) - if err != nil { - return err - } - structType, ok := typeSpec.Type.(*ast.StructType) - if !ok { - return fmt.Errorf("%s is not supported type for %s", refType, paramType) - } - refSplit := strings.Split(refType, ".") - schema, err := operation.parser.parseStruct(refSplit[0], structType.Fields) + schema, err := operation.parser.getTypeSchema(refType, astFile, false) if err != nil { return err } @@ -276,52 +271,6 @@ func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.F return nil } -func (operation *Operation) registerSchemaType(schemaType string, astFile *ast.File) (string, *ast.TypeSpec, error) { - if !strings.ContainsRune(schemaType, '.') { - if astFile == nil { - return schemaType, nil, fmt.Errorf("no package name for type %s", schemaType) - } - schemaType = fullTypeName(astFile.Name.String(), schemaType) - } - refSplit := strings.Split(schemaType, ".") - pkgName := refSplit[0] - typeName := refSplit[1] - if typeSpec, ok := operation.parser.TypeDefinitions[pkgName][typeName]; ok { - operation.parser.registerTypes[schemaType] = typeSpec - return schemaType, typeSpec, nil - } - var typeSpec *ast.TypeSpec - if astFile == nil { - return schemaType, nil, fmt.Errorf("can not register schema type: %q reason: astFile == nil", schemaType) - } - for _, imp := range astFile.Imports { - if imp.Name != nil && imp.Name.Name == pkgName { // the import had an alias that matched - break - } - impPath := strings.Replace(imp.Path.Value, `"`, ``, -1) - if strings.HasSuffix(impPath, "/"+pkgName) { - var err error - typeSpec, err = findTypeDef(impPath, typeName) - if err != nil { - return schemaType, nil, fmt.Errorf("can not find type def: %q error: %s", schemaType, err) - } - break - } - } - - if typeSpec == nil { - return schemaType, nil, fmt.Errorf("can not find schema type: %q", schemaType) - } - - if _, ok := operation.parser.TypeDefinitions[pkgName]; !ok { - operation.parser.TypeDefinitions[pkgName] = make(map[string]*ast.TypeSpec) - } - - operation.parser.TypeDefinitions[pkgName][typeName] = typeSpec - operation.parser.registerTypes[schemaType] = typeSpec - return schemaType, typeSpec, nil -} - var regexAttributes = map[string]*regexp.Regexp{ // for Enums(A, B) "enums": regexp.MustCompile(`(?i)\s+enums\(.*\)`), @@ -654,11 +603,11 @@ func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) return operation.parseCombinedObjectSchema(refType, astFile) default: if operation.parser != nil { // checking refType has existing in 'TypeDefinitions' - refNewType, typeSpec, err := operation.registerSchemaType(refType, astFile) + schema, err := operation.parser.getTypeSchema(refType, astFile, true) if err != nil { return nil, err } - refType = TypeDocName(refNewType, typeSpec) + return schema, nil } return RefSchema(refType), nil diff --git a/operation_test.go b/operation_test.go index 29f3a2c6c..184a9a25f 100644 --- a/operation_test.go +++ b/operation_test.go @@ -2,7 +2,6 @@ package swag import ( "encoding/json" - "go/ast" goparser "go/parser" "go/token" "testing" @@ -12,7 +11,7 @@ import ( ) func TestParseEmptyComment(t *testing.T) { - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment("//", nil) assert.NoError(t, err) @@ -27,7 +26,7 @@ func TestParseTagsComment(t *testing.T) { ] }` comment := `/@Tags pet, store,user` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) b, _ := json.MarshalIndent(operation, "", " ") @@ -54,7 +53,7 @@ func TestParseAcceptComment(t *testing.T) { ] }` comment := `/@Accept json,xml,plain,html,mpfd,x-www-form-urlencoded,json-api,json-stream,octet-stream,png,jpeg,gif,application/xhtml+xml,application/health+json` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) b, _ := json.MarshalIndent(operation, "", " ") @@ -64,7 +63,7 @@ func TestParseAcceptComment(t *testing.T) { func TestParseAcceptCommentErr(t *testing.T) { comment := `/@Accept unknown` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } @@ -104,7 +103,7 @@ func TestParseProduceCommentErr(t *testing.T) { func TestParseRouterComment(t *testing.T) { comment := `/@Router /customer/get-wishlist/{wishlist_id} [get]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/customer/get-wishlist/{wishlist_id}", operation.Path) @@ -113,7 +112,7 @@ func TestParseRouterComment(t *testing.T) { func TestParseRouterOnlySlash(t *testing.T) { comment := `// @Router / [get]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/", operation.Path) @@ -122,7 +121,7 @@ func TestParseRouterOnlySlash(t *testing.T) { func TestParseRouterCommentWithPlusSign(t *testing.T) { comment := `/@Router /customer/get-wishlist/{proxy+} [post]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/customer/get-wishlist/{proxy+}", operation.Path) @@ -131,7 +130,7 @@ func TestParseRouterCommentWithPlusSign(t *testing.T) { func TestParseRouterCommentWithColonSign(t *testing.T) { comment := `/@Router /customer/get-wishlist/{wishlist_id}:move [post]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) assert.Equal(t, "/customer/get-wishlist/{wishlist_id}:move", operation.Path) @@ -140,32 +139,29 @@ func TestParseRouterCommentWithColonSign(t *testing.T) { func TestParseRouterCommentNoColonSignAtPathStartErr(t *testing.T) { comment := `/@Router :customer/get-wishlist/{wishlist_id}:move [post]` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } func TestParseRouterCommentMethodSeparationErr(t *testing.T) { comment := `/@Router /api/{id}|,*[get` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } func TestParseRouterCommentMethodMissingErr(t *testing.T) { comment := `/@Router /customer/get-wishlist/{wishlist_id}` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) } func TestParseResponseCommentWithObjectType(t *testing.T) { comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() - - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["OrderRow"] = &ast.TypeSpec{} + operation := NewOperation(nil) + operation.parser.addTestType("model.OrderRow") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -190,11 +186,9 @@ func TestParseResponseCommentWithObjectType(t *testing.T) { func TestParseResponseCommentWithNestedPrimitiveType(t *testing.T) { comment := `@Success 200 {object} model.CommonHeader{data=string,data2=int} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -234,11 +228,9 @@ func TestParseResponseCommentWithNestedPrimitiveType(t *testing.T) { func TestParseResponseCommentWithNestedPrimitiveArrayType(t *testing.T) { comment := `@Success 200 {object} model.CommonHeader{data=[]string,data2=[]int} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -284,13 +276,10 @@ func TestParseResponseCommentWithNestedPrimitiveArrayType(t *testing.T) { func TestParseResponseCommentWithNestedObjectType(t *testing.T) { comment := `@Success 200 {object} model.CommonHeader{data=model.Payload,data2=model.Payload2} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() - - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload2"] = &ast.TypeSpec{} + operation := NewOperation(nil) + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -330,13 +319,11 @@ func TestParseResponseCommentWithNestedObjectType(t *testing.T) { func TestParseResponseCommentWithNestedArrayObjectType(t *testing.T) { comment := `@Success 200 {object} model.CommonHeader{data=[]model.Payload,data2=[]model.Payload2} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload2"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.Payload2") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -382,12 +369,10 @@ func TestParseResponseCommentWithNestedArrayObjectType(t *testing.T) { func TestParseResponseCommentWithNestedFields(t *testing.T) { comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload,data4=[]model.Payload} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -439,13 +424,11 @@ func TestParseResponseCommentWithNestedFields(t *testing.T) { func TestParseResponseCommentWithDeepNestedFields(t *testing.T) { comment := `@Success 200 {object} model.CommonHeader{data1=int,data2=[]int,data3=model.Payload{data1=int,data2=model.DeepPayload},data4=[]model.Payload{data1=[]int,data2=[]model.DeepPayload}} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["DeepPayload"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") + operation.parser.addTestType("model.DeepPayload") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -532,12 +515,10 @@ func TestParseResponseCommentWithDeepNestedFields(t *testing.T) { func TestParseResponseCommentWithNestedArrayMapFields(t *testing.T) { comment := `@Success 200 {object} []map[string]model.CommonHeader{data1=[]map[string]model.Payload,data2=map[string][]int} "Error message, if code != 200` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} - operation.parser.TypeDefinitions["model"]["Payload"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") + operation.parser.addTestType("model.Payload") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -594,11 +575,9 @@ func TestParseResponseCommentWithNestedArrayMapFields(t *testing.T) { func TestParseResponseCommentWithObjectTypeInSameFile(t *testing.T) { comment := `@Success 200 {object} testOwner "Error message, if code != 200"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["swag"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["swag"]["testOwner"] = &ast.TypeSpec{} + operation.parser.addTestType("swag.testOwner") fset := token.NewFileSet() astFile, err := goparser.ParseFile(fset, "operation_test.go", `package swag @@ -635,11 +614,9 @@ func TestParseResponseCommentWithObjectTypeAnonymousField(t *testing.T) { func TestParseResponseCommentWithObjectTypeErr(t *testing.T) { comment := `@Success 200 {object} model.OrderRow "Error message, if code != 200"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["notexist"] = &ast.TypeSpec{} + operation.parser.addTestType("model.notexist") err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -647,7 +624,8 @@ func TestParseResponseCommentWithObjectTypeErr(t *testing.T) { func TestParseResponseCommentWithArrayType(t *testing.T) { comment := `@Success 200 {array} model.OrderRow "Error message, if code != 200` - operation := NewOperation() + operation := NewOperation(nil) + operation.parser.addTestType("model.OrderRow") err := operation.ParseComment(comment, nil) assert.NoError(t, err) response := operation.Responses.StatusCodeResponses[200] @@ -675,7 +653,7 @@ func TestParseResponseCommentWithArrayType(t *testing.T) { func TestParseResponseCommentWithBasicType(t *testing.T) { comment := `@Success 200 {string} string "it's ok'"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") b, _ := json.MarshalIndent(operation, "", " ") @@ -695,7 +673,7 @@ func TestParseResponseCommentWithBasicType(t *testing.T) { func TestParseEmptyResponseComment(t *testing.T) { comment := `@Success 200 "it's ok"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") @@ -713,7 +691,7 @@ func TestParseEmptyResponseComment(t *testing.T) { func TestParseResponseCommentWithHeader(t *testing.T) { comment := `@Success 200 "it's ok"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") @@ -747,7 +725,7 @@ func TestParseResponseCommentWithHeader(t *testing.T) { func TestParseEmptyResponseOnlyCode(t *testing.T) { comment := `@Success 200` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err, "ParseComment should not fail") @@ -762,7 +740,7 @@ func TestParseEmptyResponseOnlyCode(t *testing.T) { } func TestParseResponseCommentParamMissing(t *testing.T) { - operation := NewOperation() + operation := NewOperation(nil) paramLenErrComment := `@Success notIntCode {string}` paramLenErr := operation.ParseComment(paramLenErrComment, nil) @@ -772,7 +750,7 @@ func TestParseResponseCommentParamMissing(t *testing.T) { // Test ParseParamComment func TestParseParamCommentByPathType(t *testing.T) { comment := `@Param some_id path int true "Some ID"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -794,7 +772,7 @@ func TestParseParamCommentByPathType(t *testing.T) { // Test ParseParamComment Query Params func TestParseParamCommentBodyArray(t *testing.T) { comment := `@Param names body []string true "Users List"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -821,7 +799,7 @@ func TestParseParamCommentBodyArray(t *testing.T) { // Test ParseParamComment Query Params func TestParseParamCommentQueryArray(t *testing.T) { comment := `@Param names query []string true "Users List"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -846,7 +824,7 @@ func TestParseParamCommentQueryArray(t *testing.T) { // Test ParseParamComment Query Params func TestParseParamCommentQueryArrayFormat(t *testing.T) { comment := `@Param names query []string true "Users List" collectionFormat(multi)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -871,7 +849,7 @@ func TestParseParamCommentQueryArrayFormat(t *testing.T) { func TestParseParamCommentByID(t *testing.T) { comment := `@Param unsafe_id[lte] query int true "Unsafe query param"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -892,7 +870,7 @@ func TestParseParamCommentByID(t *testing.T) { func TestParseParamCommentByQueryType(t *testing.T) { comment := `@Param some_id query int true "Some ID"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -913,11 +891,9 @@ func TestParseParamCommentByQueryType(t *testing.T) { func TestParseParamCommentByBodyType(t *testing.T) { comment := `@Param some_id body model.OrderRow true "Some ID"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["OrderRow"] = &ast.TypeSpec{} + operation.parser.addTestType("model.OrderRow") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -940,11 +916,9 @@ func TestParseParamCommentByBodyType(t *testing.T) { func TestParseParamCommentByBodyTypeWithDeepNestedFields(t *testing.T) { comment := `@Param body body model.CommonHeader{data=string,data2=int} true "test deep"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} + operation.parser.addTestType("model.CommonHeader") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -987,8 +961,7 @@ func TestParseParamCommentByBodyTypeWithDeepNestedFields(t *testing.T) { func TestParseParamCommentByBodyTypeArrayOfPrimitiveGo(t *testing.T) { comment := `@Param some_id body []int true "Some ID"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1014,11 +987,8 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGo(t *testing.T) { func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFields(t *testing.T) { comment := `@Param body body []model.CommonHeader{data=string,data2=int} true "test deep"` - operation := NewOperation() - operation.parser = New() - - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["CommonHeader"] = &ast.TypeSpec{} + operation := NewOperation(nil) + operation.parser.addTestType("model.CommonHeader") err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1064,11 +1034,8 @@ func TestParseParamCommentByBodyTypeArrayOfPrimitiveGoWithDeepNestedFields(t *te func TestParseParamCommentByBodyTypeErr(t *testing.T) { comment := `@Param some_id body model.OrderRow true "Some ID"` - operation := NewOperation() - operation.parser = New() - - operation.parser.TypeDefinitions["model"] = make(map[string]*ast.TypeSpec) - operation.parser.TypeDefinitions["model"]["notexist"] = &ast.TypeSpec{} + operation := NewOperation(nil) + operation.parser.addTestType("model.notexist") err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -1076,8 +1043,7 @@ func TestParseParamCommentByBodyTypeErr(t *testing.T) { func TestParseParamCommentByFormDataType(t *testing.T) { comment := `@Param file formData file true "this is a test file"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1099,8 +1065,7 @@ func TestParseParamCommentByFormDataType(t *testing.T) { func TestParseParamCommentByFormDataTypeUint64(t *testing.T) { comment := `@Param file formData uint64 true "this is a test file"` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1122,7 +1087,7 @@ func TestParseParamCommentByFormDataTypeUint64(t *testing.T) { func TestParseParamCommentByNotSupportedType(t *testing.T) { comment := `@Param some_id not_supported int true "Some ID"` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -1130,7 +1095,7 @@ func TestParseParamCommentByNotSupportedType(t *testing.T) { func TestParseParamCommentNotMatch(t *testing.T) { comment := `@Param some_id body mock true` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.Error(t, err) @@ -1138,7 +1103,7 @@ func TestParseParamCommentNotMatch(t *testing.T) { func TestParseParamCommentByEnums(t *testing.T) { comment := `@Param some_id query string true "Some ID" Enums(A, B, C)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1162,7 +1127,7 @@ func TestParseParamCommentByEnums(t *testing.T) { assert.Equal(t, expected, string(b)) comment = `@Param some_id query int true "Some ID" Enums(1, 2, 3)` - operation = NewOperation() + operation = NewOperation(nil) err = operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1186,7 +1151,7 @@ func TestParseParamCommentByEnums(t *testing.T) { assert.Equal(t, expected, string(b)) comment = `@Param some_id query number true "Some ID" Enums(1.1, 2.2, 3.3)` - operation = NewOperation() + operation = NewOperation(nil) err = operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1210,7 +1175,7 @@ func TestParseParamCommentByEnums(t *testing.T) { assert.Equal(t, expected, string(b)) comment = `@Param some_id query bool true "Some ID" Enums(true, false)` - operation = NewOperation() + operation = NewOperation(nil) err = operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1232,7 +1197,7 @@ func TestParseParamCommentByEnums(t *testing.T) { }` assert.Equal(t, expected, string(b)) - operation = NewOperation() + operation = NewOperation(nil) comment = `@Param some_id query int true "Some ID" Enums(A, B, C)` assert.Error(t, operation.ParseComment(comment, nil)) @@ -1249,7 +1214,7 @@ func TestParseParamCommentByEnums(t *testing.T) { func TestParseParamCommentByMaxLength(t *testing.T) { comment := `@Param some_id query string true "Some ID" MaxLength(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1277,7 +1242,7 @@ func TestParseParamCommentByMaxLength(t *testing.T) { func TestParseParamCommentByMinLength(t *testing.T) { comment := `@Param some_id query string true "Some ID" MinLength(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1305,7 +1270,7 @@ func TestParseParamCommentByMinLength(t *testing.T) { func TestParseParamCommentByMininum(t *testing.T) { comment := `@Param some_id query int true "Some ID" Mininum(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1333,7 +1298,7 @@ func TestParseParamCommentByMininum(t *testing.T) { func TestParseParamCommentByMaxinum(t *testing.T) { comment := `@Param some_id query int true "Some ID" Maxinum(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1362,7 +1327,7 @@ func TestParseParamCommentByMaxinum(t *testing.T) { func TestParseParamCommentByDefault(t *testing.T) { comment := `@Param some_id query int true "Some ID" Default(10)` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1384,7 +1349,7 @@ func TestParseParamCommentByDefault(t *testing.T) { func TestParseIdComment(t *testing.T) { comment := `@Id myOperationId` - operation := NewOperation() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1411,8 +1376,8 @@ func TestFindTypeDefInvalidPkg(t *testing.T) { func TestParseSecurityComment(t *testing.T) { comment := `@Security OAuth2Implicit[read, write]` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) + err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1432,8 +1397,7 @@ func TestParseSecurityComment(t *testing.T) { func TestParseMultiDescription(t *testing.T) { comment := `@Description line one` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1454,8 +1418,7 @@ func TestParseMultiDescription(t *testing.T) { func TestParseSummary(t *testing.T) { comment := `@summary line one` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1467,8 +1430,7 @@ func TestParseSummary(t *testing.T) { func TestParseDeprecationDescription(t *testing.T) { comment := `@Deprecated` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) @@ -1478,27 +1440,11 @@ func TestParseDeprecationDescription(t *testing.T) { } } -func TestRegisterSchemaType(t *testing.T) { - operation := NewOperation() - - fset := token.NewFileSet() - astFile, err := goparser.ParseFile(fset, "main.go", `package main - import "timer" -`, goparser.ParseComments) - - assert.NoError(t, err) - - operation.parser = New() - _, _, err = operation.registerSchemaType("timer.Location", astFile) - assert.Error(t, err) -} - func TestParseExtentions(t *testing.T) { // Fail if there are no args for attributes. { comment := `@x-amazon-apigateway-integration` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a value") @@ -1507,8 +1453,7 @@ func TestParseExtentions(t *testing.T) { // Fail if args of attributes are broken. { comment := `@x-amazon-apigateway-integration ["broken"}]` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.EqualError(t, err, "annotation @x-amazon-apigateway-integration need a valid json value") @@ -1517,8 +1462,7 @@ func TestParseExtentions(t *testing.T) { // OK { comment := `@x-amazon-apigateway-integration {"uri": "${some_arn}", "passthroughBehavior": "when_no_match", "httpMethod": "POST", "type": "aws_proxy"}` - operation := NewOperation() - operation.parser = New() + operation := NewOperation(nil) err := operation.ParseComment(comment, nil) assert.NoError(t, err) diff --git a/packages.go b/packages.go new file mode 100644 index 000000000..29fceb9e2 --- /dev/null +++ b/packages.go @@ -0,0 +1,234 @@ +package swag + +import ( + "go/ast" + "go/token" + "strings" +) + +//PackagesDefinitions map[package import path]*PackageDefinitions +type PackagesDefinitions struct { + files map[*ast.File]*AstFileInfo + packages map[string]*PackageDefinitions + uniqueDefinitions map[string]*TypeSpecDef +} + +//NewPackagesDefinitions create object PackagesDefinitions +func NewPackagesDefinitions() *PackagesDefinitions { + return &PackagesDefinitions{ + files: make(map[*ast.File]*AstFileInfo), + packages: make(map[string]*PackageDefinitions), + uniqueDefinitions: make(map[string]*TypeSpecDef), + } +} + +//CollectAstFile collect ast.file +func (pkgs *PackagesDefinitions) CollectAstFile(packageDir, path string, astFile *ast.File) { + if pkgs.files == nil { + pkgs.files = make(map[*ast.File]*AstFileInfo) + } + + pkgs.files[astFile] = &AstFileInfo{ + File: astFile, + Path: path, + PackagePath: packageDir, + } + + if len(packageDir) == 0 { + return + } + + if pkgs.packages == nil { + pkgs.packages = make(map[string]*PackageDefinitions) + } + + if pd, ok := pkgs.packages[packageDir]; ok { + pd.Files[path] = astFile + } else { + pkgs.packages[packageDir] = &PackageDefinitions{ + Name: astFile.Name.Name, + Files: map[string]*ast.File{path: astFile}, + TypeDefinitions: make(map[string]*TypeSpecDef), + } + } +} + +//RangeFiles for range the collection of ast.File +func (pkgs *PackagesDefinitions) RangeFiles(handle func(filename string, file *ast.File) error) error { + for file, info := range pkgs.files { + if err := handle(info.Path, file); err != nil { + return err + } + } + return nil +} + +//ParseTypes parse types +//@Return parsed definitions +func (pkgs *PackagesDefinitions) ParseTypes() (map[*TypeSpecDef]*Schema, error) { + parsedSchemas := make(map[*TypeSpecDef]*Schema) + for astFile, info := range pkgs.files { + for _, astDeclaration := range astFile.Decls { + if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { + for _, astSpec := range generalDeclaration.Specs { + if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { + typeSpecDef := &TypeSpecDef{ + PkgPath: info.PackagePath, + File: astFile, + TypeSpec: typeSpec, + } + + if idt, ok := typeSpec.Type.(*ast.Ident); ok && IsGolangPrimitiveType(idt.Name) { + parsedSchemas[typeSpecDef] = &Schema{ + PkgPath: typeSpecDef.PkgPath, + Name: astFile.Name.Name, + Schema: PrimitiveSchema(TransToValidSchemeType(idt.Name)), + } + } + + if pkgs.uniqueDefinitions == nil { + pkgs.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + + fullName := typeSpecDef.FullName() + anotherTypeDef, ok := pkgs.uniqueDefinitions[fullName] + if ok { + if typeSpecDef.PkgPath == anotherTypeDef.PkgPath { + continue + } else { + delete(pkgs.uniqueDefinitions, fullName) + } + } else { + pkgs.uniqueDefinitions[fullName] = typeSpecDef + } + + pkgs.packages[typeSpecDef.PkgPath].TypeDefinitions[typeSpecDef.Name()] = typeSpecDef + } + } + } + } + } + return parsedSchemas, nil +} + +func (pkgs *PackagesDefinitions) findTypeSpec(pkgPath string, typeName string) *TypeSpecDef { + if pkgs.packages != nil { + if pd, ok := pkgs.packages[pkgPath]; ok { + if typeSpec, ok := pd.TypeDefinitions[typeName]; ok { + return typeSpec + } + } + } + return nil +} + +// findPackagePathFromImports finds out the package path of a package via ranging imports of a ast.File +// @pkg the name of the target package +// @file current ast.File in which to search imports +// @return the package path of a package of @pkg +func (pkgs *PackagesDefinitions) findPackagePathFromImports(pkg string, file *ast.File) string { + if file == nil { + return "" + } + + if strings.ContainsRune(pkg, '.') { + pkg = strings.Split(pkg, ".")[0] + } + + hasAnonymousPkg := false + + // prior to match named package + for _, imp := range file.Imports { + if imp.Name != nil { + if imp.Name.Name == pkg { + return strings.Trim(imp.Path.Value, `"`) + } else if imp.Name.Name == "_" { + hasAnonymousPkg = true + } + } else if pkgs.packages != nil { + path := strings.Trim(imp.Path.Value, `"`) + if pd, ok := pkgs.packages[path]; ok { + if pd.Name == pkg { + return path + } + } + } + } + + //match unnamed package + if hasAnonymousPkg && pkgs.packages != nil { + for _, imp := range file.Imports { + if imp.Name == nil { + continue + } + if imp.Name.Name == "_" { + path := strings.Trim(imp.Path.Value, `"`) + if pd, ok := pkgs.packages[path]; ok { + if pd.Name == pkg { + return path + } + } + } + } + } + return "" +} + +// FindTypeSpec finds out TypeSpecDef of a type by typeName +// @typeName the name of the target type, if it starts with a package name, find its own package path from imports on top of @file +// @file the ast.file in which @typeName is used +// @pkgPath the package path of @file +func (pkgs *PackagesDefinitions) FindTypeSpec(typeName string, file *ast.File) *TypeSpecDef { + if IsGolangPrimitiveType(typeName) { + return nil + } else if file == nil { // for test + return pkgs.uniqueDefinitions[typeName] + } + + if strings.ContainsRune(typeName, '.') { + parts := strings.Split(typeName, ".") + + isAliasPkgName := func(file *ast.File, pkgName string) bool { + if file != nil && file.Imports != nil { + for _, pkg := range file.Imports { + if pkg.Name != nil && pkg.Name.Name == pkgName { + return true + } + } + } + + return false + } + + if !isAliasPkgName(file, parts[0]) { + if typeDef, ok := pkgs.uniqueDefinitions[typeName]; ok { + return typeDef + } + } + + pkgPath := pkgs.findPackagePathFromImports(parts[0], file) + if len(pkgPath) == 0 && parts[0] == file.Name.Name { + pkgPath = pkgs.files[file].PackagePath + } + return pkgs.findTypeSpec(pkgPath, parts[1]) + } + + if typeDef, ok := pkgs.uniqueDefinitions[fullTypeName(file.Name.Name, typeName)]; ok { + return typeDef + } + + if typeDef := pkgs.findTypeSpec(pkgs.files[file].PackagePath, typeName); typeDef != nil { + return typeDef + } + + for _, imp := range file.Imports { + if imp.Name != nil && imp.Name.Name == "." { + pkgPath := strings.Trim(imp.Path.Value, `"`) + if typeDef := pkgs.findTypeSpec(pkgPath, typeName); typeDef != nil { + return typeDef + } + } + } + + return nil +} diff --git a/parser.go b/parser.go index a79f8ecb1..2de6f2c3c 100644 --- a/parser.go +++ b/parser.go @@ -2,6 +2,7 @@ package swag import ( "encoding/json" + "errors" "fmt" "go/ast" "go/build" @@ -19,7 +20,6 @@ import ( "unicode" "github.com/KyleBanks/depth" - "github.com/go-openapi/jsonreference" "github.com/go-openapi/spec" ) @@ -34,25 +34,30 @@ const ( SnakeCase = "snakecase" ) +var ( + //ErrRecursiveParseStruct recursively parsing struct + ErrRecursiveParseStruct = errors.New("recursively parsing struct") + + //ErrFuncTypeField field type is func + ErrFuncTypeField = errors.New("field type is func") + + // ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type + ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type") +) + // Parser implements a parser for Go source files. type Parser struct { // swagger represents the root document object for the API specification swagger *spec.Swagger - // files is a map that stores map[real_go_file_path][astFile] - files map[string]*ast.File + //packages store entities of APIs, definitions, file, package path etc. and their relations + packages *PackagesDefinitions - // TypeDefinitions is a map that stores [package name][type name][*ast.TypeSpec] - TypeDefinitions map[string]map[string]*ast.TypeSpec + //ParsedSchemas store schemas which have been parsed from ast.TypeSpec + ParsedSchemas map[*TypeSpecDef]*Schema - // ImportAliases is map that stores [import name][import package name][*ast.ImportSpec] - ImportAliases map[string]map[string]*ast.ImportSpec - - // CustomPrimitiveTypes is a map that stores custom primitive types to actual golang types [type name][string] - CustomPrimitiveTypes map[string]string - - // registerTypes is a map that stores [refTypeName][*ast.TypeSpec] - registerTypes map[string]*ast.TypeSpec + //OutputSchemas store schemas which will be export to swagger + OutputSchemas map[*TypeSpecDef]*Schema PropNamingStrategy string @@ -65,7 +70,7 @@ type Parser struct { ParseInternal bool // structStack stores full names of the structures that were already parsed or are being parsed now - structStack []string + structStack []*TypeSpecDef // markdownFileDir holds the path to the folder, where markdown files are stored markdownFileDir string @@ -94,12 +99,10 @@ func New(options ...func(*Parser)) *Parser { Definitions: make(map[string]spec.Schema), }, }, - files: make(map[string]*ast.File), - TypeDefinitions: make(map[string]map[string]*ast.TypeSpec), - ImportAliases: make(map[string]map[string]*ast.ImportSpec), - CustomPrimitiveTypes: make(map[string]string), - registerTypes: make(map[string]*ast.TypeSpec), - excludes: make(map[string]bool), + packages: NewPackagesDefinitions(), + ParsedSchemas: make(map[*TypeSpecDef]*Schema), + OutputSchemas: make(map[*TypeSpecDef]*Schema), + excludes: make(map[string]bool), } for _, option := range options { @@ -133,7 +136,12 @@ func SetExcludedDirsAndFiles(excludes string) func(*Parser) { func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error { Printf("Generate general API Info, search dir:%s", searchDir) - if err := parser.getAllGoFileInfo(searchDir); err != nil { + packageDir, err := getPkgName(searchDir) + if err != nil { + Printf("warning: failed to get package name in dir: %s, error: %s", searchDir, err.Error()) + } + + if err := parser.getAllGoFileInfo(packageDir, searchDir); err != nil { return err } @@ -164,17 +172,12 @@ func (parser *Parser) ParseAPI(searchDir string, mainAPIFile string) error { return err } - for _, astFile := range parser.files { - parser.ParseType(astFile) - } - - for fileName, astFile := range parser.files { - if err := parser.ParseRouterAPIInfo(fileName, astFile); err != nil { - return err - } + parser.ParsedSchemas, err = parser.packages.ParseTypes() + if err != nil { + return err } - return parser.parseDefinitions() + return parser.packages.RangeFiles(parser.ParseRouterAPIInfo) } func getPkgName(searchDir string) (string, error) { @@ -489,8 +492,7 @@ func (parser *Parser) ParseRouterAPIInfo(fileName string, astFile *ast.File) err switch astDeclaration := astDescription.(type) { case *ast.FuncDecl: if astDeclaration.Doc != nil && astDeclaration.Doc.List != nil { - operation := NewOperation() //for per 'function' comment, create a new 'Operation' object - operation.parser = parser + operation := NewOperation(parser) //for per 'function' comment, create a new 'Operation' object for _, comment := range astDeclaration.Doc.List { if err := operation.ParseComment(comment.Text, astFile); err != nil { return fmt.Errorf("ParseComment error in file %s :%+v", fileName, err) @@ -527,141 +529,117 @@ func (parser *Parser) ParseRouterAPIInfo(fileName string, astFile *ast.File) err return nil } -// ParseType parses type info for given astFile. -func (parser *Parser) ParseType(astFile *ast.File) { - if _, ok := parser.TypeDefinitions[astFile.Name.String()]; !ok { - parser.TypeDefinitions[astFile.Name.String()] = make(map[string]*ast.TypeSpec) - } - - for _, astDeclaration := range astFile.Decls { - if generalDeclaration, ok := astDeclaration.(*ast.GenDecl); ok && generalDeclaration.Tok == token.TYPE { - for _, astSpec := range generalDeclaration.Specs { - if typeSpec, ok := astSpec.(*ast.TypeSpec); ok { - typeName := fmt.Sprintf("%v", typeSpec.Type) - // check if its a custom primitive type - if IsGolangPrimitiveType(typeName) { - var typeSpecFullName = fmt.Sprintf("%s.%s", astFile.Name.String(), typeSpec.Name.String()) - parser.CustomPrimitiveTypes[typeSpecFullName] = TransToValidSchemeType(typeName) - } else { - parser.TypeDefinitions[astFile.Name.String()][typeSpec.Name.String()] = typeSpec - } +func convertFromSpecificToPrimitive(typeName string) (string, error) { + name := typeName + if strings.ContainsRune(name, '.') { + name = strings.Split(name, ".")[1] + } + switch strings.ToUpper(name) { + case "TIME", "OBJECTID", "UUID": + return STRING, nil + case "DECIMAL": + return NUMBER, nil + } + return typeName, ErrFailedConvertPrimitiveType +} - } - } - } +func (parser *Parser) getTypeSchema(typeName string, file *ast.File, ref bool) (*spec.Schema, error) { + if IsGolangPrimitiveType(typeName) { + return PrimitiveSchema(TransToValidSchemeType(typeName)), nil } - for _, importSpec := range astFile.Imports { - if importSpec.Name == nil { - continue - } + if schemaType, err := convertFromSpecificToPrimitive(typeName); err == nil { + return PrimitiveSchema(schemaType), nil + } - alias := importSpec.Name.Name + typeSpecDef := parser.packages.FindTypeSpec(typeName, file) + if typeSpecDef == nil { + return nil, fmt.Errorf("cannot find type definition: %s", typeName) + } - if _, ok := parser.ImportAliases[alias]; !ok { - parser.ImportAliases[alias] = make(map[string]*ast.ImportSpec) - } + schema, ok := parser.ParsedSchemas[typeSpecDef] + if !ok { + var err error + schema, err = parser.ParseDefinition(typeSpecDef) + if err == ErrRecursiveParseStruct { + if ref { + return parser.getRefTypeSchema(typeSpecDef, schema), nil + } - importParts := strings.Split(strings.Trim(importSpec.Path.Value, "\""), "/") - importPkgName := importParts[len(importParts)-1] + } else if err != nil { + return nil, err + } + } - parser.ImportAliases[alias][importPkgName] = importSpec + if ref && len(schema.Schema.Type) > 0 && schema.Schema.Type[0] == "object" { + return parser.getRefTypeSchema(typeSpecDef, schema), nil } + return schema.Schema, nil } -func (parser *Parser) isInStructStack(refTypeName string) bool { - for _, structName := range parser.structStack { - if refTypeName == structName { - return true +func (parser *Parser) getRefTypeSchema(typeSpecDef *TypeSpecDef, schema *Schema) *spec.Schema { + if _, ok := parser.OutputSchemas[typeSpecDef]; !ok { + if _, ok := parser.swagger.Definitions[schema.Name]; ok { + schema.Name = fullTypeName(schema.PkgPath, strings.Split(schema.Name, ".")[1]) + schema.Name = strings.ReplaceAll(schema.Name, "/", "_") } + if schema.Schema != nil { + parser.swagger.Definitions[schema.Name] = *schema.Schema + } else { + parser.swagger.Definitions[schema.Name] = spec.Schema{} + } + parser.OutputSchemas[typeSpecDef] = schema } - return false + + return RefSchema(schema.Name) } -// parseDefinitions parses Swagger Api definitions. -func (parser *Parser) parseDefinitions() error { - // sort the typeNames so that parsing definitions is deterministic - typeNames := make([]string, 0, len(parser.registerTypes)) - for refTypeName := range parser.registerTypes { - typeNames = append(typeNames, refTypeName) - } - sort.Strings(typeNames) - - for _, refTypeName := range typeNames { - typeSpec := parser.registerTypes[refTypeName] - ss := strings.Split(refTypeName, ".") - pkgName := ss[0] - parser.structStack = nil - if err := parser.ParseDefinition(pkgName, typeSpec.Name.Name, typeSpec); err != nil { - return err +func (parser *Parser) isInStructStack(typeSpecDef *TypeSpecDef) bool { + for _, specDef := range parser.structStack { + if typeSpecDef == specDef { + return true } } - return nil + return false } // ParseDefinition parses given type spec that corresponds to the type under // given name and package, and populates swagger schema definitions registry // with a schema for the given type -func (parser *Parser) ParseDefinition(pkgName, typeName string, typeSpec *ast.TypeSpec) error { - refTypeName := TypeDocName(pkgName, typeSpec) +func (parser *Parser) ParseDefinition(typeSpecDef *TypeSpecDef) (*Schema, error) { + typeName := typeSpecDef.FullName() + refTypeName := TypeDocName(typeName, typeSpecDef.TypeSpec) - if typeSpec == nil { - Println("Skipping '" + refTypeName + "', pkg '" + pkgName + "' not found, try add flag --parseDependency or --parseVendor.") - return nil + if schema, ok := parser.ParsedSchemas[typeSpecDef]; ok { + Println("Skipping '" + typeName + "', already parsed.") + return schema, nil } - if _, isParsed := parser.swagger.Definitions[refTypeName]; isParsed { - Println("Skipping '" + refTypeName + "', already parsed.") - return nil + if parser.isInStructStack(typeSpecDef) { + Println("Skipping '" + typeName + "', recursion detected.") + return &Schema{ + Name: refTypeName, + PkgPath: typeSpecDef.PkgPath, + Schema: PrimitiveSchema(OBJECT)}, + ErrRecursiveParseStruct } + parser.structStack = append(parser.structStack, typeSpecDef) - if parser.isInStructStack(refTypeName) { - Println("Skipping '" + refTypeName + "', recursion detected.") - return nil - } - parser.structStack = append(parser.structStack, refTypeName) + Println("Generating " + typeName) - Println("Generating " + refTypeName) - - schema, err := parser.parseTypeExpr(pkgName, typeName, typeSpec.Type) + schema, err := parser.parseTypeExpr(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false) if err != nil { - return err - } - parser.swagger.Definitions[refTypeName] = *schema - return nil -} - -func (parser *Parser) collectRequiredFields(pkgName string, properties map[string]spec.Schema, extraRequired []string) (requiredFields []string) { - // created sorted list of properties keys so when we iterate over them it's deterministic - ks := make([]string, 0, len(properties)) - for k := range properties { - ks = append(ks, k) - } - sort.Strings(ks) - - requiredFields = make([]string, 0) - - // iterate over keys list instead of map to avoid the random shuffle of the order that go does for maps - for _, k := range ks { - prop := properties[k] - - // todo find the pkgName of the property type - tname := prop.SchemaProps.Type[0] - if _, ok := parser.TypeDefinitions[pkgName][tname]; ok { - tspec := parser.TypeDefinitions[pkgName][tname] - parser.ParseDefinition(pkgName, tname, tspec) - } - requiredFields = append(requiredFields, prop.SchemaProps.Required...) - properties[k] = prop + return nil, err } + s := &Schema{Name: refTypeName, PkgPath: typeSpecDef.PkgPath, Schema: schema} + parser.ParsedSchemas[typeSpecDef] = s - if extraRequired != nil { - requiredFields = append(requiredFields, extraRequired...) + //update an empty schema as a result of recursion + if s2, ok := parser.OutputSchemas[typeSpecDef]; ok { + parser.swagger.Definitions[s2.Name] = *schema } - sort.Strings(requiredFields) - - return + return s, nil } func fullTypeName(pkgName, typeName string) string { @@ -673,67 +651,44 @@ func fullTypeName(pkgName, typeName string) string { // parseTypeExpr parses given type expression that corresponds to the type under // given name and package, and returns swagger schema for it. -func (parser *Parser) parseTypeExpr(pkgName, typeName string, typeExpr ast.Expr) (*spec.Schema, error) { - +func (parser *Parser) parseTypeExpr(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.Schema, error) { switch expr := typeExpr.(type) { // type Foo struct {...} case *ast.StructType: - if typedef, ok := parser.TypeDefinitions[pkgName][typeName]; ok { - refTypeName := TypeDocName(pkgName, typedef) - if schema, isParsed := parser.swagger.Definitions[refTypeName]; isParsed { - return &schema, nil - } - } - - return parser.parseStruct(pkgName, expr.Fields) + return parser.parseStruct(file, expr.Fields) // type Foo Baz case *ast.Ident: - if IsGolangPrimitiveType(expr.Name) { - return &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: spec.StringOrArray{TransToValidSchemeType(expr.Name)}, - }, - }, nil - } - refTypeName := fullTypeName(pkgName, expr.Name) - if typedef, ok := parser.TypeDefinitions[pkgName][expr.Name]; ok { - refTypeName = TypeDocName(pkgName, typedef) - if _, isParsed := parser.swagger.Definitions[refTypeName]; !isParsed { - parser.ParseDefinition(pkgName, expr.Name, typedef) - } - } - - return RefSchema(refTypeName), nil + return parser.getTypeSchema(expr.Name, file, ref) // type Foo *Baz case *ast.StarExpr: - return parser.parseTypeExpr(pkgName, typeName, expr.X) + return parser.parseTypeExpr(file, expr.X, ref) + // type Foo pkg.Bar + case *ast.SelectorExpr: + if xIdent, ok := expr.X.(*ast.Ident); ok { + return parser.getTypeSchema(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref) + } // type Foo []Baz case *ast.ArrayType: - itemSchema, err := parser.parseTypeExpr(pkgName, "", expr.Elt) + itemSchema, err := parser.parseTypeExpr(file, expr.Elt, true) if err != nil { - return &spec.Schema{}, err + return nil, err } return spec.ArrayProperty(itemSchema), nil - - // type Foo pkg.Bar - case *ast.SelectorExpr: - if xIdent, ok := expr.X.(*ast.Ident); ok { - return parser.parseTypeExpr(xIdent.Name, expr.Sel.Name, expr.Sel) - } - // type Foo map[string]Bar case *ast.MapType: if _, ok := expr.Value.(*ast.InterfaceType); ok { return spec.MapProperty(nil), nil } - schema, err := parser.parseTypeExpr(pkgName, "", expr.Value) + schema, err := parser.parseTypeExpr(file, expr.Value, true) if err != nil { - return &spec.Schema{}, err + return nil, err } return spec.MapProperty(schema), nil + case *ast.FuncType: + return nil, ErrFuncTypeField // ... default: Printf("Type definition of type '%T' is not supported yet. Using 'object' instead.\n", typeExpr) @@ -742,29 +697,26 @@ func (parser *Parser) parseTypeExpr(pkgName, typeName string, typeExpr ast.Expr) return PrimitiveSchema(OBJECT), nil } -func (parser *Parser) parseStruct(pkgName string, fields *ast.FieldList) (*spec.Schema, error) { +func (parser *Parser) parseStruct(file *ast.File, fields *ast.FieldList) (*spec.Schema, error) { - extraRequired := make([]string, 0) + required := make([]string, 0) properties := make(map[string]spec.Schema) for _, field := range fields.List { - fieldProps, requiredFromAnon, err := parser.parseStructField(pkgName, field) - if err != nil { - return &spec.Schema{}, err + fieldProps, requiredFromAnon, err := parser.parseStructField(file, field) + if err == ErrFuncTypeField { + continue + } else if err != nil { + return nil, err + } else if len(fieldProps) == 0 { + continue } - extraRequired = append(extraRequired, requiredFromAnon...) + required = append(required, requiredFromAnon...) for k, v := range fieldProps { properties[k] = v } } - // collect requireds from our properties and anonymous fields - required := parser.collectRequiredFields(pkgName, properties, extraRequired) - - // unset required from properties because we've collected them - for k, prop := range properties { - prop.SchemaProps.Required = make([]string, 0) - properties[k] = prop - } + sort.Strings(required) return &spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -821,277 +773,93 @@ func (sf *structField) toStandardSchema() *spec.Schema { } } -func (parser *Parser) parseStructField(pkgName string, field *ast.Field) (map[string]spec.Schema, []string, error) { - properties := map[string]spec.Schema{} - +func (parser *Parser) parseStructField(file *ast.File, field *ast.Field) (map[string]spec.Schema, []string, error) { if field.Names == nil { - fullTypeName, err := getFieldType(field.Type) + typeName, err := getFieldType(field.Type) if err != nil { - return properties, []string{}, nil + return nil, nil, err } - - typeName := fullTypeName - - if splits := strings.Split(fullTypeName, "."); len(splits) > 1 { - pkgName = splits[0] - typeName = splits[1] + schema, err := parser.getTypeSchema(typeName, file, false) + if err != nil { + return nil, nil, err } - - typeSpec := parser.TypeDefinitions[pkgName][typeName] - if typeSpec == nil { - // Check if the pkg name is an alias and try to define type spec using real package name - if aliases, ok := parser.ImportAliases[pkgName]; ok { - for alias := range aliases { - typeSpec = parser.TypeDefinitions[alias][typeName] - if typeSpec != nil { - break - } - } - } - } - if typeSpec != nil { - schema, err := parser.parseTypeExpr(pkgName, typeName, typeSpec.Type) - if err != nil { - return properties, []string{}, err - } - schemaType := "unknown" - if len(schema.SchemaProps.Type) > 0 { - schemaType = schema.SchemaProps.Type[0] - } - - switch schemaType { - case OBJECT: - for k, v := range schema.SchemaProps.Properties { - properties[k] = v - } - case ARRAY: - properties[typeName] = *schema - default: - Printf("Can't extract properties from a schema of type '%s'", schemaType) + if len(schema.Type) > 0 && schema.Type[0] == "object" && len(schema.Properties) > 0 { + properties := map[string]spec.Schema{} + for k, v := range schema.Properties { + properties[k] = v } return properties, schema.SchemaProps.Required, nil } - - return properties, nil, nil + //for alias type of non-struct types ,such as array,map, etc. ignore field tag. + return map[string]spec.Schema{typeName: *schema}, nil, nil } - structField, err := parser.parseField(pkgName, field) + fieldName, schema, err := parser.getFieldName(field) if err != nil { - return properties, nil, err - } - if structField.name == "" { - return properties, nil, nil + return nil, nil, err + } else if fieldName == "" { + return nil, nil, nil + } else if schema == nil { + typeName, err := getFieldType(field.Type) + if err == nil { + //named type + schema, err = parser.getTypeSchema(typeName, file, true) + } else { + //unnamed type + schema, err = parser.parseTypeExpr(file, field.Type, false) + } + if err != nil { + return nil, nil, err + } } - // TODO: find package of schemaType and/or arrayType - if structField.crossPkg != "" { - pkgName = structField.crossPkg + types := parser.GetSchemaTypePath(schema, 2) + if len(types) == 0 { + return nil, nil, fmt.Errorf("invalid type for field: %s", field.Names[0]) } - fillObject := func(src, dest interface{}) error { - bin, err := json.Marshal(src) - if err != nil { - return err - } - return json.Unmarshal(bin, dest) + structField, err := parser.parseFieldTag(field, types) + if err != nil { + return nil, nil, err } - //for spec.Schema have implemented json.Marshaler, here in another way to convert - fillSchema := func(src, dest *spec.Schema) error { - err = fillObject(&src.SchemaProps, &dest.SchemaProps) - if err != nil { - return err - } - err = fillObject(&src.SwaggerSchemaProps, &dest.SwaggerSchemaProps) - if err != nil { - return err - } - return fillObject(&src.VendorExtensible, &dest.VendorExtensible) + if structField.schemaType == "string" && types[0] != structField.schemaType { + schema = PrimitiveSchema(structField.schemaType) } - if typeSpec, ok := parser.TypeDefinitions[pkgName][structField.schemaType]; ok { // user type field - // write definition if not yet present - err = parser.ParseDefinition(pkgName, structField.schemaType, typeSpec) - if err != nil { - return properties, nil, err - } - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{OBJECT}, // to avoid swagger validation error - Description: structField.desc, - Required: required, - Ref: spec.Ref{ - Ref: jsonreference.MustCreateRef("#/definitions/" + TypeDocName(pkgName, typeSpec)), - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else if structField.schemaType == ARRAY { // array field type - // if defined -- ref it - if typeSpec, ok := parser.TypeDefinitions[pkgName][structField.arrayType]; ok { // user type in array - parser.ParseDefinition(pkgName, structField.arrayType, - parser.TypeDefinitions[pkgName][structField.arrayType]) - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - schema := spec.ArrayProperty(RefSchema(TypeDocName(pkgName, typeSpec))) - schema.Description = structField.desc - schema.Required = required - schema.ReadOnly = structField.readOnly - properties[structField.name] = *schema - } else if structField.arrayType == OBJECT { - // Anonymous struct - if astTypeArray, ok := field.Type.(*ast.ArrayType); ok { // if array - props := make(map[string]spec.Schema) - if expr, ok := astTypeArray.Elt.(*ast.StructType); ok { - for _, field := range expr.Fields.List { - var fieldProps map[string]spec.Schema - fieldProps, _, err = parser.parseStructField(pkgName, field) - if err != nil { - return properties, nil, err - } - for k, v := range fieldProps { - props[k] = v - } - } - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{OBJECT}, - Properties: props, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - ReadOnly: structField.readOnly, - }, - } - } else { - schema, _ := parser.parseTypeExpr(pkgName, "", astTypeArray.Elt) - schema = spec.ArrayProperty(schema) - schema.Description = structField.desc - schema.ReadOnly = structField.readOnly - properties[structField.name] = *schema - } - } - } else if structField.arrayType == ARRAY { - if astTypeArray, ok := field.Type.(*ast.ArrayType); ok { - schema, _ := parser.parseTypeExpr(pkgName, "", astTypeArray.Elt) - schema = spec.ArrayProperty(schema) - schema.Description = structField.desc - schema.ReadOnly = structField.readOnly - properties[structField.name] = *schema - } - } else { - // standard type in array - required := make([]string, 0) - if structField.isRequired { - required = append(required, structField.name) - } - - properties[structField.name] = spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.schemaType}, - Description: structField.desc, - Format: structField.formatType, - Required: required, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{structField.arrayType}, - Maximum: structField.maximum, - Minimum: structField.minimum, - MaxLength: structField.maxLength, - MinLength: structField.minLength, - Enum: structField.enums, - Default: structField.defaultValue, - }, - }, - }, - }, - SwaggerSchemaProps: spec.SwaggerSchemaProps{ - Example: structField.exampleValue, - ReadOnly: structField.readOnly, - }, - } - } - } else if astTypeMap, ok := field.Type.(*ast.MapType); ok { // if map - stdSchema := structField.toStandardSchema() - mapValueSchema, err := parser.parseTypeExpr(pkgName, "", astTypeMap) - if err != nil { - return properties, nil, err - } - stdSchema.Type = mapValueSchema.Type - stdSchema.AdditionalProperties = mapValueSchema.AdditionalProperties - properties[structField.name] = *stdSchema - } else { - stdSchema := structField.toStandardSchema() - properties[structField.name] = *stdSchema - - if nestStar, ok := field.Type.(*ast.StarExpr); ok { - if !IsGolangPrimitiveType(structField.schemaType) { - schema, err := parser.parseTypeExpr(pkgName, structField.schemaType, nestStar.X) - if err != nil { - return properties, nil, err - } + schema.Description = structField.desc + schema.ReadOnly = structField.readOnly + schema.Default = structField.defaultValue + schema.Example = structField.exampleValue + schema.Format = structField.formatType + schema.Extensions = structField.extensions + eleSchema := schema + if structField.schemaType == "array" { + eleSchema = schema.Items.Schema + } + eleSchema.Maximum = structField.maximum + eleSchema.Minimum = structField.minimum + eleSchema.MaxLength = structField.maxLength + eleSchema.MinLength = structField.minLength + eleSchema.Enum = structField.enums - if len(schema.SchemaProps.Type) > 0 { - err = fillSchema(schema, stdSchema) - if err != nil { - return properties, nil, err - } - properties[structField.name] = *stdSchema - return properties, nil, nil - } - } - } else if nestStruct, ok := field.Type.(*ast.StructType); ok { - props := map[string]spec.Schema{} - nestRequired := make([]string, 0) - for _, v := range nestStruct.Fields.List { - p, _, err := parser.parseStructField(pkgName, v) - if err != nil { - return properties, nil, err - } - for k, v := range p { - if v.SchemaProps.Type[0] != OBJECT { - nestRequired = append(nestRequired, v.SchemaProps.Required...) - v.SchemaProps.Required = make([]string, 0) - } - props[k] = v - } - } - stdSchema.Properties = props - stdSchema.Required = nestRequired - properties[structField.name] = *stdSchema - } + var tagRequired []string + if structField.isRequired { + tagRequired = append(tagRequired, fieldName) } - return properties, nil, nil + return map[string]spec.Schema{fieldName: *schema}, tagRequired, nil } -func getFieldType(field interface{}) (string, error) { - +func getFieldType(field ast.Expr) (string, error) { switch ftype := field.(type) { case *ast.Ident: return ftype.Name, nil - case *ast.SelectorExpr: packageName, err := getFieldType(ftype.X) if err != nil { return "", err } - return fmt.Sprintf("%s.%s", packageName, ftype.Sel.Name), nil + return fullTypeName(packageName, ftype.Sel.Name), nil case *ast.StarExpr: fullName, err := getFieldType(ftype.X) @@ -1099,53 +867,60 @@ func getFieldType(field interface{}) (string, error) { return "", err } return fullName, nil - } return "", fmt.Errorf("unknown field type %#v", field) } -func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField, error) { - prop, err := getPropertyName(pkgName, field.Type, parser) - if err != nil { - return nil, err +func (parser *Parser) getFieldName(field *ast.Field) (name string, schema *spec.Schema, err error) { + // Skip non-exported fields. + if !ast.IsExported(field.Names[0].Name) { + return "", nil, nil } - if len(prop.ArrayType) == 0 { - if err := CheckSchemaType(prop.SchemaType); err != nil { - return nil, err + if field.Tag != nil { + // `json:"tag"` -> json:"tag" + structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1)) + if ignoreTag := structTag.Get("swaggerignore"); strings.EqualFold(ignoreTag, "true") { + return "", nil, nil } - } else { - if err := CheckSchemaType(ARRAY); err != nil { - return nil, err + + name = structTag.Get("json") + // json:"tag,hoge" + if name = strings.TrimSpace(strings.Split(name, ",")[0]); name == "-" { + return "", nil, nil } - } - // Skip func fields. - if prop.SchemaType == FUNC { - return &structField{name: ""}, nil + if typeTag := structTag.Get("swaggertype"); typeTag != "" { + parts := strings.Split(typeTag, ",") + schema, err = BuildCustomSchema(parts) + if err != nil { + return "", nil, err + } + } } - // Skip non-exported fields. - if !ast.IsExported(field.Names[0].Name) { - return &structField{name: ""}, nil + if name == "" { + switch parser.PropNamingStrategy { + case SnakeCase: + name = toSnakeCase(field.Names[0].Name) + case PascalCase: + name = field.Names[0].Name + case CamelCase: + name = toLowerCamelCase(field.Names[0].Name) + default: + name = toLowerCamelCase(field.Names[0].Name) + } } + return name, schema, err +} +func (parser *Parser) parseFieldTag(field *ast.Field, types []string) (*structField, error) { structField := &structField{ - name: field.Names[0].Name, - schemaType: prop.SchemaType, - arrayType: prop.ArrayType, - crossPkg: prop.CrossPkg, - } - - switch parser.PropNamingStrategy { - case SnakeCase: - structField.name = toSnakeCase(structField.name) - case PascalCase: - //use struct field name - case CamelCase: - structField.name = toLowerCamelCase(structField.name) - default: - structField.name = toLowerCamelCase(structField.name) + // name: field.Names[0].Name, + schemaType: types[0], + } + if len(types) > 1 && types[0] == "array" { + structField.arrayType = types[1] } if field.Doc != nil { @@ -1161,59 +936,10 @@ func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField // `json:"tag"` -> json:"tag" structTag := reflect.StructTag(strings.Replace(field.Tag.Value, "`", "", -1)) - if ignoreTag := structTag.Get("swaggerignore"); ignoreTag == "true" { - structField.name = "" - return structField, nil - } - jsonTag := structTag.Get("json") - hasStringTag := false - // json:"tag,hoge" - if strings.Contains(jsonTag, ",") { - // json:"name,string" or json:",string" - if strings.Contains(jsonTag, ",string") { - hasStringTag = true - } + // json:"name,string" or json:",string" + hasStringTag := strings.Contains(jsonTag, ",string") - // json:",hoge" - if strings.HasPrefix(jsonTag, ",") { - jsonTag = "" - } else { - jsonTag = strings.SplitN(jsonTag, ",", 2)[0] - } - } - if jsonTag == "-" { - structField.name = "" - return structField, nil - } else if jsonTag != "" { - structField.name = jsonTag - } - - if typeTag := structTag.Get("swaggertype"); typeTag != "" { - parts := strings.Split(typeTag, ",") - if 0 < len(parts) && len(parts) <= 2 { - newSchemaType := parts[0] - newArrayType := structField.arrayType - if len(parts) >= 2 { - if newSchemaType == ARRAY { - newArrayType = parts[1] - if err := CheckSchemaType(newArrayType); err != nil { - return nil, err - } - } else if newSchemaType == PRIMITIVE { - newSchemaType = parts[1] - newArrayType = parts[1] - } - } - - if err := CheckSchemaType(newSchemaType); err != nil { - return nil, err - } - - structField.schemaType = newSchemaType - structField.arrayType = newArrayType - } - } if exampleTag := structTag.Get("example"); exampleTag != "" { if hasStringTag { // then the example must be in string format @@ -1335,6 +1061,36 @@ func (parser *Parser) parseField(pkgName string, field *ast.Field) (*structField return structField, nil } +// GetSchemaTypePath get path of schema type +func (parser *Parser) GetSchemaTypePath(schema *spec.Schema, depth int) []string { + if schema == nil || depth == 0 { + return nil + } + if name := schema.Ref.String(); name != "" { + if pos := strings.LastIndexByte(name, '/'); pos >= 0 { + name = name[pos+1:] + if schema, ok := parser.swagger.Definitions[name]; ok { + return parser.GetSchemaTypePath(&schema, depth) + } + } + } else if len(schema.Type) > 0 { + if schema.Type[0] == "array" { + depth-- + s := []string{schema.Type[0]} + return append(s, parser.GetSchemaTypePath(schema.Items.Schema, depth)...) + } else if schema.Type[0] == "object" { + if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { + // for map + depth-- + s := []string{schema.Type[0]} + return append(s, parser.GetSchemaTypePath(schema.AdditionalProperties.Schema, depth)...) + } + } + return []string{schema.Type[0]} + } + return nil +} + func replaceLastTag(slice []spec.Tag, element spec.Tag) { slice = slice[:len(slice)-1] slice = append(slice, element) @@ -1440,8 +1196,20 @@ func defineTypeOfExample(schemaType, arrayType, exampleValue string) (interface{ } // GetAllGoFileInfo gets all Go source files information for given searchDir. -func (parser *Parser) getAllGoFileInfo(searchDir string) error { - return filepath.Walk(searchDir, parser.visit) +func (parser *Parser) getAllGoFileInfo(packageDir, searchDir string) error { + return filepath.Walk(searchDir, func(path string, f os.FileInfo, err error) error { + if err := parser.Skip(path, f); err != nil { + return err + } else if f.IsDir() { + return nil + } + + relPath, err := filepath.Rel(searchDir, path) + if err != nil { + return err + } + return parser.parseFile(filepath.ToSlash(filepath.Dir(filepath.Clean(filepath.Join(packageDir, relPath)))), path, nil) + }) } func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { @@ -1466,7 +1234,7 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { } path := filepath.Join(srcDir, f.Name()) - if err := parser.parseFile(path); err != nil { + if err := parser.parseFile(pkg.Name, path, nil); err != nil { return err } } @@ -1480,23 +1248,17 @@ func (parser *Parser) getAllGoFileInfoFromDeps(pkg *depth.Pkg) error { return nil } -func (parser *Parser) visit(path string, f os.FileInfo, err error) error { - if err := parser.Skip(path, f); err != nil { - return err +func (parser *Parser) parseFile(packageDir, path string, src interface{}) error { + if strings.HasSuffix(strings.ToLower(path), "_test.go") || filepath.Ext(path) != ".go" { + return nil } - return parser.parseFile(path) -} - -func (parser *Parser) parseFile(path string) error { - if ext := filepath.Ext(path); ext == ".go" { - fset := token.NewFileSet() // positions are relative to fset - astFile, err := goparser.ParseFile(fset, path, nil, goparser.ParseComments) - if err != nil { - return fmt.Errorf("ParseFile error:%+v", err) - } - parser.files[path] = astFile + // positions are relative to FileSet + astFile, err := goparser.ParseFile(token.NewFileSet(), path, src, goparser.ParseComments) + if err != nil { + return fmt.Errorf("ParseFile error:%+v", err) } + parser.packages.CollectAstFile(packageDir, path, astFile) return nil } @@ -1523,3 +1285,20 @@ func (parser *Parser) Skip(path string, f os.FileInfo) error { func (parser *Parser) GetSwagger() *spec.Swagger { return parser.swagger } + +//addTestType just for tests +func (parser *Parser) addTestType(typename string) { + if parser.ParsedSchemas == nil { + parser.ParsedSchemas = make(map[*TypeSpecDef]*Schema) + } + if parser.packages.uniqueDefinitions == nil { + parser.packages.uniqueDefinitions = make(map[string]*TypeSpecDef) + } + typeDef := &TypeSpecDef{} + parser.packages.uniqueDefinitions[typename] = typeDef + parser.ParsedSchemas[typeDef] = &Schema{ + PkgPath: "", + Name: typename, + Schema: PrimitiveSchema(OBJECT), + } +} diff --git a/parser_test.go b/parser_test.go index c172dab6d..19471b67f 100644 --- a/parser_test.go +++ b/parser_test.go @@ -249,646 +249,44 @@ func TestGetAllGoFileInfo(t *testing.T) { searchDir := "testdata/pet" p := New() - err := p.getAllGoFileInfo(searchDir) + err := p.getAllGoFileInfo("testdata", searchDir) assert.NoError(t, err) - assert.NotEmpty(t, p.files[filepath.Join("testdata", "pet", "main.go")]) - assert.NotEmpty(t, p.files[filepath.Join("testdata", "pet", "web", "handler.go")]) - assert.Equal(t, 2, len(p.files)) + assert.Equal(t, 2, len(p.packages.files)) } func TestParser_ParseType(t *testing.T) { searchDir := "testdata/simple/" p := New() - err := p.getAllGoFileInfo(searchDir) + err := p.getAllGoFileInfo("testdata", searchDir) assert.NoError(t, err) - for _, file := range p.files { - p.ParseType(file) - } - - assert.NotNil(t, p.TypeDefinitions["api"]["Pet3"]) - assert.NotNil(t, p.TypeDefinitions["web"]["Pet"]) - assert.NotNil(t, p.TypeDefinitions["web"]["Pet2"]) -} - -func TestGetSchemes(t *testing.T) { - schemes := getSchemes("@schemes http https") - expectedSchemes := []string{"http", "https"} - assert.Equal(t, expectedSchemes, schemes) -} - -func TestParseSimpleApi1(t *testing.T) { - expected := `{ - "swagger": "2.0", - "info": { - "description": "This is a sample server Petstore server.", - "title": "Swagger Example API", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "name": "API Support", - "url": "http://www.swagger.io/support", - "email": "support@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0" - }, - "host": "petstore.swagger.io", - "basePath": "/v2", - "paths": { - "/file/upload": { - "post": { - "description": "Upload file", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "summary": "Upload file", - "operationId": "file.upload", - "parameters": [ - { - "type": "file", - "description": "this is a test file", - "name": "file", - "in": "formData", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } - }, - "400": { - "description": "We need ID!!", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" - } - } - } - } - }, - "/testapi/get-string-by-int/{some_id}": { - "get": { - "description": "get string by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Add a new pet to the store", - "operationId": "get-string-by-int", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "Some ID", - "name": "some_id", - "in": "path", - "required": true - }, - { - "description": "Some ID", - "name": "some_id", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/web.Pet" - } - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } - }, - "400": { - "description": "We need ID!!", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" - } - } - } - } - }, - "/testapi/get-struct-array-by-string/{some_id}": { - "get": { - "security": [ - { - "ApiKeyAuth": [] - }, - { - "BasicAuth": [] - }, - { - "OAuth2Application": [ - "write" - ] - }, - { - "OAuth2Implicit": [ - "read", - "admin" - ] - }, - { - "OAuth2AccessCode": [ - "read" - ] - }, - { - "OAuth2Password": [ - "admin" - ] - } - ], - "description": "get struct array by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "operationId": "get-struct-array-by-string", - "parameters": [ - { - "type": "string", - "description": "Some ID", - "name": "some_id", - "in": "path", - "required": true - }, - { - "enum": [ - 1, - 2, - 3 - ], - "type": "integer", - "description": "Category", - "name": "category", - "in": "query", - "required": true - }, - { - "minimum": 0, - "type": "integer", - "default": 0, - "description": "Offset", - "name": "offset", - "in": "query", - "required": true - }, - { - "maximum": 50, - "type": "integer", - "default": 10, - "description": "Limit", - "name": "limit", - "in": "query", - "required": true - }, - { - "maxLength": 50, - "minLength": 1, - "type": "string", - "default": "\"\"", - "description": "q", - "name": "q", - "in": "query", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } - }, - "400": { - "description": "We need ID!!", - "schema": { - "$ref": "#/definitions/web.APIError" - } - }, - "404": { - "description": "Can not find ID", - "schema": { - "$ref": "#/definitions/web.APIError" - } - } - } - } - } - }, - "definitions": { - "api.SwagReturn": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "cross.Cross": { - "type": "object", - "properties": { - "Array": { - "type": "array", - "items": { - "type": "string" - } - }, - "String": { - "type": "string" - } - } - }, - "web.APIError": { - "type": "object", - "properties": { - "CreatedAt": { - "type": "string" - }, - "ErrorCode": { - "type": "integer" - }, - "ErrorMessage": { - "type": "string" - } - } - }, - "web.AnonymousStructArray": { - "type": "array", - "items": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - } - }, - "web.CrossAlias": { - "$ref": "#/definitions/cross.Cross" - }, - "web.IndirectRecursiveTest": { - "type": "object", - "properties": { - "Tags": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Tag" - } - } - } - }, - "web.Pet": { - "type": "object", - "required": [ - "name", - "photo_urls" - ], - "properties": { - "category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "example": "category_name" - }, - "photo_urls": { - "type": "array", - "format": "url", - "items": { - "type": "string" - }, - "example": [ - "http://test/image/1.jpg", - "http://test/image/2.jpg" - ] - }, - "small_category": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "id": { - "type": "integer", - "example": 1 - }, - "name": { - "type": "string", - "maxLength": 16, - "minLength": 4, - "example": "detail_category_name" - }, - "photo_urls": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "http://test/image/1.jpg", - "http://test/image/2.jpg" - ] - } - } - } - } - }, - "data": { - "type": "object" - }, - "decimal": { - "type": "number" - }, - "enum_array": { - "type": "array", - "items": { - "type": "integer", - "enum": [ - 1, - 2, - 3, - 5, - 7 - ] - } - }, - "id": { - "type": "integer", - "format": "int64", - "readOnly": true, - "example": 1 - }, - "int_array": { - "type": "array", - "items": { - "type": "integer" - }, - "example": [ - 1, - 2 - ] - }, - "is_alive": { - "type": "boolean", - "default": true, - "example": true - }, - "name": { - "type": "string", - "example": "poti" - }, - "pets": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Pet2" - } - }, - "pets2": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Pet2" - } - }, - "photo_urls": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "http://test/image/1.jpg", - "http://test/image/2.jpg" - ] - }, - "price": { - "type": "number", - "maximum": 1000, - "minimum": 1, - "example": 3.25 - }, - "status": { - "type": "string", - "enum": [ - "healthy", - "ill" - ] - }, - "tags": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Tag" - } - }, - "uuid": { - "type": "string" - } - } - }, - "web.Pet2": { - "type": "object", - "properties": { - "deleted_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "middlename": { - "type": "string", - "x-abc": "def", - "x-nullable": true - } - } - }, - "web.Pet5a": { - "type": "object", - "required": [ - "name", - "odd" - ], - "properties": { - "name": { - "type": "string" - }, - "odd": { - "type": "boolean" - } - } - }, - "web.Pet5b": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "name": { - "type": "string" - } - } - }, - "web.Pet5c": { - "type": "object", - "required": [ - "name", - "odd" - ], - "properties": { - "name": { - "type": "string" - }, - "odd": { - "type": "boolean" - } - } - }, - "web.RevValue": { - "type": "object", - "properties": { - "Data": { - "type": "integer" - }, - "Err": { - "type": "integer" - }, - "Status": { - "type": "boolean" - }, - "cross": { - "type": "object", - "$ref": "#/definitions/cross.Cross" - }, - "crosses": { - "type": "array", - "items": { - "$ref": "#/definitions/cross.Cross" - } - } - } - }, - "web.Tag": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - }, - "pets": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Pet" - } - } - } - }, - "web.Tags": { - "type": "array", - "items": { - "$ref": "#/definitions/web.Tag" - } - } - }, - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - }, - "BasicAuth": { - "type": "basic" - }, - "OAuth2AccessCode": { - "type": "oauth2", - "flow": "accessCode", - "authorizationUrl": "https://example.com/oauth/authorize", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information" - } - }, - "OAuth2Application": { - "type": "oauth2", - "flow": "application", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Implicit": { - "type": "oauth2", - "flow": "implicit", - "authorizationUrl": "https://example.com/oauth/authorize", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Password": { - "type": "oauth2", - "flow": "password", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "read": " Grants read access", - "write": " Grants write access" - } - } - } -}` + p.packages.ParseTypes() + + assert.NotNil(t, p.packages.uniqueDefinitions["api.Pet3"]) + assert.NotNil(t, p.packages.uniqueDefinitions["web.Pet"]) + assert.NotNil(t, p.packages.uniqueDefinitions["web.Pet2"]) +} + +func TestGetSchemes(t *testing.T) { + schemes := getSchemes("@schemes http https") + expectedSchemes := []string{"http", "https"} + assert.Equal(t, expectedSchemes, schemes) +} + +func TestParseSimpleApi1(t *testing.T) { + expected, err := ioutil.ReadFile("testdata/simple/expected.json") + assert.NoError(t, err) searchDir := "testdata/simple" mainAPIFile := "main.go" p := New() p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) + err = p.ParseAPI(searchDir, mainAPIFile) assert.NoError(t, err) - b, _ := json.MarshalIndent(p.swagger, "", " ") - assert.Equal(t, expected, string(b)) + b, _ := json.MarshalIndent(p.swagger, "", " ") + assert.Equal(t, string(expected), string(b)) } func TestParseSimpleApi_ForSnakecase(t *testing.T) { @@ -2026,158 +1424,6 @@ func TestParsePetApi(t *testing.T) { assert.Equal(t, expected, string(b)) } -func TestParseModelNotUnderRoot(t *testing.T) { - expected := `{ - "swagger": "2.0", - "info": { - "description": "This is a sample server Petstore server.", - "title": "Swagger Example API", - "termsOfService": "http://swagger.io/terms/", - "contact": { - "name": "API Support", - "url": "http://www.swagger.io/support", - "email": "support@swagger.io" - }, - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.html" - }, - "version": "1.0" - }, - "host": "petstore.swagger.io", - "basePath": "/v2", - "paths": { - "/file/upload": { - "post": { - "description": "Upload file", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Upload file", - "operationId": "file.upload", - "parameters": [ - { - "description": "Foo to create", - "name": "data", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/data.Foo" - } - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "type": "string" - } - } - } - } - }, - "/testapi/get-string-by-int/{some_id}": { - "get": { - "description": "get string by ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Add a new pet to the store", - "operationId": "get-string-by-int", - "parameters": [ - { - "type": "integer", - "format": "int64", - "description": "Some ID", - "name": "some_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "ok", - "schema": { - "$ref": "#/definitions/data.Foo" - } - } - } - } - } - }, - "definitions": { - "data.Foo": { - "type": "object", - "properties": { - "field1": { - "type": "string" - } - } - } - }, - "securityDefinitions": { - "ApiKeyAuth": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - }, - "BasicAuth": { - "type": "basic" - }, - "OAuth2AccessCode": { - "type": "oauth2", - "flow": "accessCode", - "authorizationUrl": "https://example.com/oauth/authorize", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information" - } - }, - "OAuth2Application": { - "type": "oauth2", - "flow": "application", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Implicit": { - "type": "oauth2", - "flow": "implicit", - "authorizationUrl": "https://example.com/oauth/authorize", - "scopes": { - "admin": " Grants read and write access to administrative information", - "write": " Grants write access" - } - }, - "OAuth2Password": { - "type": "oauth2", - "flow": "password", - "tokenUrl": "https://example.com/oauth/token", - "scopes": { - "admin": " Grants read and write access to administrative information", - "read": " Grants read access", - "write": " Grants write access" - } - } - } -}` - searchDir := "testdata/model_not_under_root/cmd" - mainAPIFile := "main.go" - p := New() - err := p.ParseAPI(searchDir, mainAPIFile) - assert.NoError(t, err) - b, _ := json.MarshalIndent(p.swagger, "", " ") - assert.Equal(t, expected, string(b)) -} - func TestParseModelAsTypeAlias(t *testing.T) { expected := `{ "swagger": "2.0", @@ -2349,12 +1595,11 @@ func Test(){ assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) + p.packages.CollectAstFile("api", "api/api.go", f) + _, err = p.packages.ParseTypes() assert.NoError(t, err) - typeSpec := p.TypeDefinitions["api"]["Response"] - err = p.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2413,17 +1658,15 @@ type ResponseWrapper struct { f, err := goparser.ParseFile(token.NewFileSet(), "", src, goparser.ParseComments) assert.NoError(t, err) - parser.ParseType(f) + parser.packages.CollectAstFile("api", "api/api.go", f) f2, err := goparser.ParseFile(token.NewFileSet(), "", restsrc, goparser.ParseComments) assert.NoError(t, err) - parser.ParseType(f2) + parser.packages.CollectAstFile("rest", "rest/rest.go", f2) - err = parser.ParseRouterAPIInfo("", f) - assert.NoError(t, err) + parser.packages.ParseTypes() - typeSpec := parser.TypeDefinitions["api"]["Response"] - err = parser.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = parser.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(parser.swagger.Definitions, "", " ") @@ -2469,7 +1712,6 @@ func Test(){ }, "test2": { "description": "test2", - "type": "object", "$ref": "#/definitions/api.Child" } } @@ -2480,12 +1722,11 @@ func Test(){ assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) + p.packages.CollectAstFile("api", "api/api.go", f) + _, err = p.packages.ParseTypes() assert.NoError(t, err) - typeSpec := p.TypeDefinitions["api"]["Parent"] - err = p.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2573,7 +1814,6 @@ func Test(){ }, "test6": { "description": "test6", - "type": "object", "$ref": "#/definitions/api.MyMapType" }, "test7": { @@ -2607,12 +1847,11 @@ func Test(){ assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) - assert.NoError(t, err) + p.packages.CollectAstFile("api", "api/api.go", f) + + p.packages.ParseTypes() - typeSpec := p.TypeDefinitions["api"]["Parent"] - err = p.ParseDefinition("api", typeSpec.Name.Name, typeSpec) + err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) out, err := json.MarshalIndent(p.swagger.Definitions, "", " ") @@ -2830,64 +2069,6 @@ func Test3(){ assert.NotNil(t, val.Delete) } -func TestSkip(t *testing.T) { - folder1 := "/tmp/vendor" - err := os.Mkdir(folder1, os.ModePerm) - assert.NoError(t, err) - f1, _ := os.Stat(folder1) - - parser := New() - - assert.True(t, parser.Skip(folder1, f1) == filepath.SkipDir) - assert.NoError(t, os.Remove(folder1)) - - folder2 := "/tmp/.git" - err = os.Mkdir(folder2, os.ModePerm) - assert.NoError(t, err) - f2, _ := os.Stat(folder2) - - assert.True(t, parser.Skip(folder2, f2) == filepath.SkipDir) - assert.NoError(t, os.Remove(folder2)) - - currentPath := "./" - currentPathInfo, _ := os.Stat(currentPath) - assert.True(t, parser.Skip(currentPath, currentPathInfo) == nil) -} - -func TestSkipMustParseVendor(t *testing.T) { - folder1 := "/tmp/vendor" - err := os.Mkdir(folder1, os.ModePerm) - assert.NoError(t, err) - - f1, _ := os.Stat(folder1) - - parser := New() - parser.ParseVendor = true - - assert.True(t, parser.Skip(folder1, f1) == nil) - assert.NoError(t, os.Remove(folder1)) - - folder2 := "/tmp/.git" - err = os.Mkdir(folder2, os.ModePerm) - assert.NoError(t, err) - - f2, _ := os.Stat(folder2) - - assert.True(t, parser.Skip(folder2, f2) == filepath.SkipDir) - assert.NoError(t, os.Remove(folder2)) - - currentPath := "./" - currentPathInfo, _ := os.Stat(currentPath) - assert.True(t, parser.Skip(currentPath, currentPathInfo) == nil) - - folder3 := "/tmp/test/vendor/github.com/swaggo/swag" - assert.NoError(t, os.MkdirAll(folder3, os.ModePerm)) - f3, _ := os.Stat(folder3) - - assert.Nil(t, parser.Skip(folder3, f3)) - assert.NoError(t, os.RemoveAll("/tmp/test")) -} - // func TestParseDeterministic(t *testing.T) { // mainAPIFile := "main.go" // for _, searchDir := range []string{ @@ -3067,7 +2248,8 @@ func Fun() { assert.NoError(t, err) p := New() - p.ParseType(f) + p.packages.CollectAstFile("api", "api/api.go", f) + p.packages.ParseTypes() err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) @@ -3099,14 +2281,11 @@ func Fun() { assert.NoError(t, err) p := New() - p.ParseType(f) - err = p.ParseRouterAPIInfo("", f) - assert.NoError(t, err) - + p.packages.CollectAstFile("api", "api/api.go", f) + p.packages.ParseTypes() err = p.ParseRouterAPIInfo("", f) assert.NoError(t, err) - err = p.parseDefinitions() assert.NoError(t, err) teacher, ok := p.swagger.Definitions["Teacher"] assert.True(t, ok) diff --git a/property.go b/property.go deleted file mode 100644 index 1f0b851e4..000000000 --- a/property.go +++ /dev/null @@ -1,139 +0,0 @@ -package swag - -import ( - "errors" - "fmt" - "go/ast" - "strings" -) - -// ErrFailedConvertPrimitiveType Failed to convert for swag to interpretable type -var ErrFailedConvertPrimitiveType = errors.New("swag property: failed convert primitive type") - -type propertyName struct { - SchemaType string - ArrayType string - CrossPkg string -} - -type propertyNewFunc func(schemeType string, crossPkg string) propertyName - -func newArrayProperty(schemeType string, crossPkg string) propertyName { - return propertyName{ - SchemaType: ARRAY, - ArrayType: schemeType, - CrossPkg: crossPkg, - } -} - -func newProperty(schemeType string, crossPkg string) propertyName { - return propertyName{ - SchemaType: schemeType, - ArrayType: "string", - CrossPkg: crossPkg, - } -} - -func convertFromSpecificToPrimitive(typeName string) (string, error) { - typeName = strings.ToUpper(typeName) - switch typeName { - case "TIME", "OBJECTID", "UUID": - return STRING, nil - case "DECIMAL": - return NUMBER, nil - } - return "", ErrFailedConvertPrimitiveType -} - -func parseFieldSelectorExpr(astTypeSelectorExpr *ast.SelectorExpr, parser *Parser, propertyNewFunc propertyNewFunc) propertyName { - if primitiveType, err := convertFromSpecificToPrimitive(astTypeSelectorExpr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - - if pkgName, ok := astTypeSelectorExpr.X.(*ast.Ident); ok { - if typeDefinitions, ok := parser.TypeDefinitions[pkgName.Name][astTypeSelectorExpr.Sel.Name]; ok { - if expr, ok := typeDefinitions.Type.(*ast.SelectorExpr); ok { - if primitiveType, err := convertFromSpecificToPrimitive(expr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - } - parser.ParseDefinition(pkgName.Name, astTypeSelectorExpr.Sel.Name, typeDefinitions) - return propertyNewFunc(astTypeSelectorExpr.Sel.Name, pkgName.Name) - } - if aliasedNames, ok := parser.ImportAliases[pkgName.Name]; ok { - for aliasedName := range aliasedNames { - if typeDefinitions, ok := parser.TypeDefinitions[aliasedName][astTypeSelectorExpr.Sel.Name]; ok { - if expr, ok := typeDefinitions.Type.(*ast.SelectorExpr); ok { - if primitiveType, err := convertFromSpecificToPrimitive(expr.Sel.Name); err == nil { - return propertyNewFunc(primitiveType, "") - } - } - parser.ParseDefinition(aliasedName, astTypeSelectorExpr.Sel.Name, typeDefinitions) - return propertyNewFunc(astTypeSelectorExpr.Sel.Name, aliasedName) - } - } - } - name := fmt.Sprintf("%s.%v", pkgName, astTypeSelectorExpr.Sel.Name) - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[name]; isCustomType { - return propertyName{SchemaType: actualPrimitiveType, ArrayType: actualPrimitiveType} - } - } - return propertyName{SchemaType: "string", ArrayType: "string"} -} - -// getPropertyName returns the string value for the given field if it exists -// allowedValues: array, boolean, integer, null, number, object, string -func getPropertyName(pkgName string, expr ast.Expr, parser *Parser) (propertyName, error) { - switch tp := expr.(type) { - case *ast.SelectorExpr: - return parseFieldSelectorExpr(tp, parser, newProperty), nil - case *ast.StarExpr: - return getPropertyName(pkgName, tp.X, parser) - case *ast.ArrayType: - return getArrayPropertyName(pkgName, tp.Elt, parser), nil - case *ast.MapType, *ast.StructType, *ast.InterfaceType: - return propertyName{SchemaType: OBJECT, ArrayType: OBJECT}, nil - case *ast.FuncType: - return propertyName{SchemaType: FUNC, ArrayType: ""}, nil - case *ast.Ident: - name := tp.Name - // check if it is a custom type - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[fullTypeName(pkgName, name)]; isCustomType { - return propertyName{SchemaType: actualPrimitiveType, ArrayType: actualPrimitiveType}, nil - } - - name = TransToValidSchemeType(name) - return propertyName{SchemaType: name, ArrayType: name}, nil - default: - return propertyName{}, errors.New("not supported" + fmt.Sprint(expr)) - } -} - -func getArrayPropertyName(pkgName string, astTypeArrayElt ast.Expr, parser *Parser) propertyName { - switch elt := astTypeArrayElt.(type) { - case *ast.StructType, *ast.MapType, *ast.InterfaceType: - return propertyName{SchemaType: ARRAY, ArrayType: OBJECT} - case *ast.ArrayType: - return propertyName{SchemaType: ARRAY, ArrayType: ARRAY} - case *ast.StarExpr: - return getArrayPropertyName(pkgName, elt.X, parser) - case *ast.SelectorExpr: - return parseFieldSelectorExpr(elt, parser, newArrayProperty) - case *ast.Ident: - name := elt.Name - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[fullTypeName(pkgName, name)]; isCustomType { - name = actualPrimitiveType - } else { - name = TransToValidSchemeType(elt.Name) - } - return propertyName{SchemaType: ARRAY, ArrayType: name} - default: - name := fmt.Sprintf("%s", astTypeArrayElt) - if actualPrimitiveType, isCustomType := parser.CustomPrimitiveTypes[fullTypeName(pkgName, name)]; isCustomType { - name = actualPrimitiveType - } else { - name = TransToValidSchemeType(name) - } - return propertyName{SchemaType: ARRAY, ArrayType: name} - } -} diff --git a/property_test.go b/property_test.go deleted file mode 100644 index 7b6f05f90..000000000 --- a/property_test.go +++ /dev/null @@ -1,322 +0,0 @@ -package swag - -import ( - "go/ast" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGetPropertyNameSelectorExpr(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "time", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "Time", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - STRING, - STRING, - "", - } - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentObjectId(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "ObjectId", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - STRING, - STRING, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentUUID(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "uuid", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - STRING, - STRING, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentDecimal(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "Decimal", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - NUMBER, - STRING, - "", - } - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameIdentTime(t *testing.T) { - input := &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "Time", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - STRING, - STRING, - "", - } - - propertyName, err := getPropertyName("test", input, nil) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameStarExprIdent(t *testing.T) { - input := &ast.StarExpr{ - Star: 1026, - X: &ast.Ident{ - NamePos: 1027, - Name: "string", - Obj: (*ast.Object)(nil), - }, - } - expected := propertyName{ - STRING, - STRING, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameStarExprMap(t *testing.T) { - input := &ast.StarExpr{ - Star: 1026, - X: &ast.MapType{ - Map: 1027, - Key: &ast.Ident{ - NamePos: 1034, - Name: "string", - Obj: (*ast.Object)(nil), - }, - Value: &ast.Ident{ - NamePos: 1041, - Name: "string", - Obj: (*ast.Object)(nil), - }, - }, - } - expected := propertyName{ - OBJECT, - OBJECT, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameArrayStarExpr(t *testing.T) { - input := &ast.ArrayType{ - Lbrack: 465, - Len: nil, - Elt: &ast.StarExpr{ - X: &ast.Ident{ - NamePos: 467, - Name: "string", - Obj: (*ast.Object)(nil), - }, - }, - } - expected := propertyName{ - ARRAY, - STRING, - "", - } - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameArrayStarExprSelector(t *testing.T) { - input := &ast.ArrayType{ - Lbrack: 1111, - Len: nil, - Elt: &ast.StarExpr{ - X: &ast.SelectorExpr{ - X: &ast.Ident{ - NamePos: 1136, - Name: "hoge", - Obj: (*ast.Object)(nil), - }, - Sel: &ast.Ident{ - NamePos: 1141, - Name: "ObjectId", - Obj: (*ast.Object)(nil), - }, - }, - }, - } - expected := propertyName{ - ARRAY, - STRING, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameArrayStructType(t *testing.T) { - input := &ast.ArrayType{ - Lbrack: 1111, - Len: nil, - Elt: &ast.StructType{}, - } - expected := propertyName{ - ARRAY, - OBJECT, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameMap(t *testing.T) { - input := &ast.MapType{ - Key: &ast.Ident{ - Name: "string", - }, - Value: &ast.Ident{ - Name: "string", - }, - } - expected := propertyName{ - OBJECT, - OBJECT, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameStruct(t *testing.T) { - input := &ast.StructType{} - expected := propertyName{ - OBJECT, - OBJECT, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameInterface(t *testing.T) { - input := &ast.InterfaceType{} - expected := propertyName{ - OBJECT, - OBJECT, - "", - } - - propertyName, err := getPropertyName("test", input, New()) - assert.NoError(t, err) - assert.Equal(t, expected, propertyName) -} - -func TestGetPropertyNameChannel(t *testing.T) { - input := &ast.ChanType{} - _, err := getPropertyName("test", input, New()) - assert.Error(t, err) -} - -func TestParseTag(t *testing.T) { - searchDir := "testdata/tags" - mainAPIFile := "main.go" - p := New(SetMarkdownFileDirectory(searchDir)) - p.PropNamingStrategy = PascalCase - err := p.ParseAPI(searchDir, mainAPIFile) - assert.NoError(t, err) - - if len(p.swagger.Tags) != 3 { - t.Log(len(p.swagger.Tags)) - t.Log("Number of tags did not match") - t.FailNow() - } - - dogs := p.swagger.Tags[0] - if dogs.TagProps.Name != "dogs" || dogs.TagProps.Description != "Dogs are cool" { - t.Log("Failed to parse dogs name or description") - t.FailNow() - } - - cats := p.swagger.Tags[1] - if cats.TagProps.Name != "cats" || cats.TagProps.Description != "Cats are the devil" { - t.Log("Failed to parse cats name or description") - t.FailNow() - } -} diff --git a/schema.go b/schema.go index db701b5a8..d43e322f0 100644 --- a/schema.go +++ b/schema.go @@ -1,6 +1,7 @@ package swag import ( + "errors" "fmt" "github.com/go-openapi/spec" "go/ast" @@ -145,3 +146,33 @@ func RefSchema(refType string) *spec.Schema { func PrimitiveSchema(refType string) *spec.Schema { return &spec.Schema{SchemaProps: spec.SchemaProps{Type: []string{refType}}} } + +// BuildCustomSchema build custom schema specified by tag swaggertype +func BuildCustomSchema(types []string) (*spec.Schema, error) { + if len(types) == 0 { + return nil, nil + } + + switch types[0] { + case "primitive": + if len(types) == 1 { + return nil, errors.New("need primitive type after primitive") + } + return BuildCustomSchema(types[1:]) + case "array": + if len(types) == 1 { + return nil, errors.New("need array item type after array") + } + schema, err := BuildCustomSchema(types[1:]) + if err != nil { + return nil, err + } + return spec.ArrayProperty(schema), nil + default: + err := CheckSchemaType(types[0]) + if err != nil { + return nil, err + } + return PrimitiveSchema(types[0]), nil + } +} diff --git a/testdata/alias_import/expected.json b/testdata/alias_import/expected.json index 1b477398c..fab871b41 100644 --- a/testdata/alias_import/expected.json +++ b/testdata/alias_import/expected.json @@ -45,7 +45,6 @@ "type": "object", "properties": { "application": { - "type": "object", "$ref": "#/definitions/types.Application" }, "application_array": { diff --git a/testdata/model_not_under_root/cmd/api/api.go b/testdata/model_not_under_root/cmd/api/api.go deleted file mode 100644 index efc56c736..000000000 --- a/testdata/model_not_under_root/cmd/api/api.go +++ /dev/null @@ -1,34 +0,0 @@ -package api - -import ( - "log" - - "github.com/gin-gonic/gin" - "github.com/swaggo/swag/testdata/model_not_under_root/data" -) - -// @Summary Add a new pet to the store -// @Description get string by ID -// @ID get-string-by-int -// @Accept json -// @Produce json -// @Param some_id path int true "Some ID" Format(int64) -// @Success 200 {object} data.Foo "ok" -// @Router /testapi/get-string-by-int/{some_id} [get] -func GetStringByInt(c *gin.Context) { - var foo data.Foo - log.Println(foo) - //write your code -} - -// @Summary Upload file -// @Description Upload file -// @ID file.upload -// @Accept json -// @Produce json -// @Param data body data.Foo true "Foo to create" -// @Success 200 {string} string "ok" -// @Router /file/upload [post] -func Upload(ctx *gin.Context) { - //write your code -} diff --git a/testdata/model_not_under_root/cmd/main.go b/testdata/model_not_under_root/cmd/main.go deleted file mode 100644 index 03d7c3621..000000000 --- a/testdata/model_not_under_root/cmd/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "github.com/gin-gonic/gin" - "github.com/swaggo/swag/testdata/model_not_under_root/cmd/api" -) - -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server Petstore server. -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @host petstore.swagger.io -// @BasePath /v2 - -// @securityDefinitions.basic BasicAuth - -// @securityDefinitions.apikey ApiKeyAuth -// @in header -// @name Authorization - -// @securitydefinitions.oauth2.application OAuth2Application -// @tokenUrl https://example.com/oauth/token -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.implicit OAuth2Implicit -// @authorizationurl https://example.com/oauth/authorize -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.password OAuth2Password -// @tokenUrl https://example.com/oauth/token -// @scope.read Grants read access -// @scope.write Grants write access -// @scope.admin Grants read and write access to administrative information - -// @securitydefinitions.oauth2.accessCode OAuth2AccessCode -// @tokenUrl https://example.com/oauth/token -// @authorizationurl https://example.com/oauth/authorize -// @scope.admin Grants read and write access to administrative information -func main() { - r := gin.New() - r.GET("/testapi/get-string-by-int/:some_id", api.GetStringByInt) - r.Run() -} diff --git a/testdata/model_not_under_root/data/foo.go b/testdata/model_not_under_root/data/foo.go deleted file mode 100644 index 7fe7208f9..000000000 --- a/testdata/model_not_under_root/data/foo.go +++ /dev/null @@ -1,5 +0,0 @@ -package data - -type Foo struct { - Field1 string `json:"field1"` -} diff --git a/testdata/nested/expected.json b/testdata/nested/expected.json index e6ebb1cc4..9de5ed335 100644 --- a/testdata/nested/expected.json +++ b/testdata/nested/expected.json @@ -66,11 +66,9 @@ "type": "string" }, "insideData": { - "type": "object", "$ref": "#/definitions/api.Bar" }, "outsideData": { - "type": "object", "$ref": "#/definitions/nested2.Body" } } diff --git a/testdata/simple/api/api.go b/testdata/simple/api/api.go index ba6734d71..7a307bb06 100644 --- a/testdata/simple/api/api.go +++ b/testdata/simple/api/api.go @@ -2,6 +2,7 @@ package api import ( "github.com/gin-gonic/gin" + _ "github.com/swaggo/swag/testdata/simple/web" ) // @Summary Add a new pet to the store @@ -59,33 +60,39 @@ func Upload(ctx *gin.Context) { // @Summary use Anonymous field // @Success 200 {object} web.RevValue "ok" +// @Router /AnonymousField [get] func AnonymousField() { } // @Summary use pet2 // @Success 200 {object} web.Pet2 "ok" +// @Router /Pet2 [get] func Pet2() { } // @Summary Use IndirectRecursiveTest // @Success 200 {object} web.IndirectRecursiveTest +// @Router /IndirectRecursiveTest [get] func IndirectRecursiveTest() { } // @Summary Use Tags // @Success 200 {object} web.Tags +// @Router /Tags [get] func Tags() { } // @Summary Use CrossAlias // @Success 200 {object} web.CrossAlias +// @Router /CrossAlias [get] func CrossAlias() { } // @Summary Use AnonymousStructArray // @Success 200 {object} web.AnonymousStructArray +// @Router /AnonymousStructArray [get] func AnonymousStructArray() { } @@ -94,16 +101,19 @@ type Pet3 struct { } // @Success 200 {object} web.Pet5a "ok" +// @Router /GetPet5a [get] func GetPet5a() { } // @Success 200 {object} web.Pet5b "ok" +// @Router /GetPet5b [get] func GetPet5b() { } // @Success 200 {object} web.Pet5c "ok" +// @Router /GetPet5c [get] func GetPet5c() { } @@ -111,6 +121,7 @@ func GetPet5c() { type SwagReturn []map[string]string // @Success 200 {object} api.SwagReturn "ok" +// @Router /GetPet6MapString [get] func GetPet6MapString() { } diff --git a/testdata/simple/expected.json b/testdata/simple/expected.json new file mode 100644 index 000000000..19cb8e01f --- /dev/null +++ b/testdata/simple/expected.json @@ -0,0 +1,727 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server.", + "title": "Swagger Example API", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "API Support", + "url": "http://www.swagger.io/support", + "email": "support@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "paths": { + "/AnonymousField": { + "get": { + "summary": "use Anonymous field", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.RevValue" + } + } + } + } + }, + "/AnonymousStructArray": { + "get": { + "summary": "Use AnonymousStructArray", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + } + } + } + } + }, + "/CrossAlias": { + "get": { + "summary": "Use CrossAlias", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.CrossAlias" + } + } + } + } + }, + "/GetPet5a": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet5a" + } + } + } + } + }, + "/GetPet5b": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet5b" + } + } + } + } + }, + "/GetPet5c": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet5c" + } + } + } + } + }, + "/GetPet6MapString": { + "get": { + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "/IndirectRecursiveTest": { + "get": { + "summary": "Use IndirectRecursiveTest", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/web.IndirectRecursiveTest" + } + } + } + } + }, + "/Pet2": { + "get": { + "summary": "use pet2", + "responses": { + "200": { + "description": "ok", + "schema": { + "$ref": "#/definitions/web.Pet2" + } + } + } + } + }, + "/Tags": { + "get": { + "summary": "Use Tags", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Tag" + } + } + } + } + } + }, + "/file/upload": { + "post": { + "description": "Upload file", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "summary": "Upload file", + "operationId": "file.upload", + "parameters": [ + { + "type": "file", + "description": "this is a test file", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + }, + "/testapi/get-string-by-int/{some_id}": { + "get": { + "description": "get string by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Add a new pet to the store", + "operationId": "get-string-by-int", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Some ID", + "name": "some_id", + "in": "path", + "required": true + }, + { + "description": "Some ID", + "name": "some_id", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/web.Pet" + } + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + }, + "/testapi/get-struct-array-by-string/{some_id}": { + "get": { + "security": [ + { + "ApiKeyAuth": [] + }, + { + "BasicAuth": [] + }, + { + "OAuth2Application": [ + "write" + ] + }, + { + "OAuth2Implicit": [ + "read", + "admin" + ] + }, + { + "OAuth2AccessCode": [ + "read" + ] + }, + { + "OAuth2Password": [ + "admin" + ] + } + ], + "description": "get struct array by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "operationId": "get-struct-array-by-string", + "parameters": [ + { + "type": "string", + "description": "Some ID", + "name": "some_id", + "in": "path", + "required": true + }, + { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "Category", + "name": "category", + "in": "query", + "required": true + }, + { + "minimum": 0, + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query", + "required": true + }, + { + "maximum": 50, + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query", + "required": true + }, + { + "maxLength": 50, + "minLength": 1, + "type": "string", + "default": "\"\"", + "description": "q", + "name": "q", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "ok", + "schema": { + "type": "string" + } + }, + "400": { + "description": "We need ID!!", + "schema": { + "$ref": "#/definitions/web.APIError" + } + }, + "404": { + "description": "Can not find ID", + "schema": { + "$ref": "#/definitions/web.APIError" + } + } + } + } + } + }, + "definitions": { + "cross.Cross": { + "type": "object", + "properties": { + "Array": { + "type": "array", + "items": { + "type": "string" + } + }, + "String": { + "type": "string" + } + } + }, + "web.APIError": { + "type": "object", + "properties": { + "CreatedAt": { + "type": "string" + }, + "ErrorCode": { + "type": "integer" + }, + "ErrorMessage": { + "type": "string" + } + } + }, + "web.CrossAlias": { + "type": "object", + "properties": { + "Array": { + "type": "array", + "items": { + "type": "string" + } + }, + "String": { + "type": "string" + } + } + }, + "web.IndirectRecursiveTest": { + "type": "object", + "properties": { + "Tags": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Tag" + } + } + } + }, + "web.Pet": { + "type": "object", + "required": [ + "name", + "photo_urls" + ], + "properties": { + "category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "example": "category_name" + }, + "photo_urls": { + "type": "array", + "format": "url", + "items": { + "type": "string" + }, + "example": [ + "http://test/image/1.jpg", + "http://test/image/2.jpg" + ] + }, + "small_category": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string", + "maxLength": 16, + "minLength": 4, + "example": "detail_category_name" + }, + "photo_urls": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "http://test/image/1.jpg", + "http://test/image/2.jpg" + ] + } + } + } + } + }, + "data": { + "type": "object" + }, + "decimal": { + "type": "number" + }, + "enum_array": { + "type": "array", + "items": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 5, + 7 + ] + } + }, + "id": { + "type": "integer", + "format": "int64", + "readOnly": true, + "example": 1 + }, + "int_array": { + "type": "array", + "items": { + "type": "integer" + }, + "example": [ + 1, + 2 + ] + }, + "is_alive": { + "type": "boolean", + "default": true, + "example": true + }, + "name": { + "type": "string", + "example": "poti" + }, + "pets": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Pet2" + } + }, + "pets2": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Pet2" + } + }, + "photo_urls": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "http://test/image/1.jpg", + "http://test/image/2.jpg" + ] + }, + "price": { + "type": "number", + "maximum": 1000, + "minimum": 1, + "example": 3.25 + }, + "status": { + "type": "string", + "enum": [ + "healthy", + "ill" + ] + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Tag" + } + }, + "uuid": { + "type": "string" + } + } + }, + "web.Pet2": { + "type": "object", + "properties": { + "deleted_at": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "middlename": { + "type": "string", + "x-abc": "def", + "x-nullable": true + } + } + }, + "web.Pet5a": { + "type": "object", + "required": [ + "name", + "odd" + ], + "properties": { + "name": { + "type": "string" + }, + "odd": { + "type": "boolean" + } + } + }, + "web.Pet5b": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "web.Pet5c": { + "type": "object", + "required": [ + "name", + "odd" + ], + "properties": { + "name": { + "type": "string" + }, + "odd": { + "type": "boolean" + } + } + }, + "web.RevValue": { + "type": "object", + "properties": { + "Data": { + "type": "integer" + }, + "Err": { + "type": "integer" + }, + "Status": { + "type": "boolean" + }, + "cross": { + "$ref": "#/definitions/cross.Cross" + }, + "crosses": { + "type": "array", + "items": { + "$ref": "#/definitions/cross.Cross" + } + } + } + }, + "web.Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "pets": { + "type": "array", + "items": { + "$ref": "#/definitions/web.Pet" + } + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + }, + "BasicAuth": { + "type": "basic" + }, + "OAuth2AccessCode": { + "type": "oauth2", + "flow": "accessCode", + "authorizationUrl": "https://example.com/oauth/authorize", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "admin": " Grants read and write access to administrative information" + } + }, + "OAuth2Application": { + "type": "oauth2", + "flow": "application", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "admin": " Grants read and write access to administrative information", + "write": " Grants write access" + } + }, + "OAuth2Implicit": { + "type": "oauth2", + "flow": "implicit", + "authorizationUrl": "https://example.com/oauth/authorize", + "scopes": { + "admin": " Grants read and write access to administrative information", + "write": " Grants write access" + } + }, + "OAuth2Password": { + "type": "oauth2", + "flow": "password", + "tokenUrl": "https://example.com/oauth/token", + "scopes": { + "admin": " Grants read and write access to administrative information", + "read": " Grants read access", + "write": " Grants write access" + } + } + } +} \ No newline at end of file diff --git a/types.go b/types.go new file mode 100644 index 000000000..cf849e2bf --- /dev/null +++ b/types.go @@ -0,0 +1,59 @@ +package swag + +import ( + "github.com/go-openapi/spec" + "go/ast" +) + +//Schema parsed schema +type Schema struct { + PkgPath string //package import path used to rename Name of a definition int case of conflict + Name string //Name in definitions + *spec.Schema // +} + +//TypeSpecDef the whole information of a typeSpec +type TypeSpecDef struct { + //path of package starting from under ${GOPATH}/src or from module path in go.mod + PkgPath string + + //ast file where TypeSpec is + File *ast.File + + //the TypeSpec of this type definition + TypeSpec *ast.TypeSpec +} + +//Name name of the typeSpec +func (t *TypeSpecDef) Name() string { + return t.TypeSpec.Name.Name +} + +//FullName full name of the typeSpec +func (t *TypeSpecDef) FullName() string { + return fullTypeName(t.File.Name.Name, t.TypeSpec.Name.Name) +} + +//AstFileInfo information of a ast.File +type AstFileInfo struct { + //File ast.File + File *ast.File + + //Path path of the ast.File + Path string + + //PackagePath package import path of the ast.File + PackagePath string +} + +//PackageDefinitions files and definition in a package +type PackageDefinitions struct { + //package name + Name string + + //files in this package, map key is file's relative path starting package path + Files map[string]*ast.File + + //definitions in this package, map key is typeName + TypeDefinitions map[string]*TypeSpecDef +}