package swag import ( "encoding/json" "fmt" "go/ast" "go/token" "net/http" "reflect" "regexp" "sort" "strings" "github.com/pkg/errors" "github.com/sv-tools/openapi/spec" ) // FieldParserFactoryV3 create FieldParser. type FieldParserFactoryV3 func(ps *Parser, file *ast.File, field *ast.Field) FieldParserV3 // FieldParserV3 parse struct field. type FieldParserV3 interface { ShouldSkip() bool FieldName() (string, error) FormName() string CustomSchema() (*spec.RefOrSpec[spec.Schema], error) ComplementSchema(schema *spec.RefOrSpec[spec.Schema]) error IsRequired() (bool, error) } // GetOpenAPI returns *spec.OpenAPI which is the root document object for the API specification. func (p *Parser) GetOpenAPI() *spec.OpenAPI { return p.openAPI } var ( serversURLPattern = regexp.MustCompile(`\{([^}]+)\}`) serversVariablesPattern = regexp.MustCompile(`^(\w+)\s+(.+)$`) ) func (p *Parser) parseGeneralAPIInfoV3(comments []string) error { previousAttribute := "" // parsing classic meta data model for line := 0; line < len(comments); line++ { commentLine := comments[line] commentLine = strings.TrimSpace(commentLine) if len(commentLine) == 0 { continue } fields := FieldsByAnySpace(commentLine, 2) attribute := fields[0] var value string if len(fields) > 1 { value = fields[1] } switch attr := strings.ToLower(attribute); attr { case versionAttr, titleAttr, tosAttr, licNameAttr, licURLAttr, conNameAttr, conURLAttr, conEmailAttr: setspecInfo(p.openAPI, attr, value) case descriptionAttr: if previousAttribute == attribute { p.openAPI.Info.Spec.Description += "\n" + value continue } setspecInfo(p.openAPI, attr, value) case descriptionMarkdownAttr: commentInfo, err := getMarkdownForTag("api", p.markdownFileDir) if err != nil { return err } setspecInfo(p.openAPI, attr, string(commentInfo)) case "@host": if len(p.openAPI.Servers) == 0 { server := spec.NewServer() server.Spec.URL = value p.openAPI.Servers = append(p.openAPI.Servers, server) } println("@host is deprecated use servers instead") case "@basepath": if len(p.openAPI.Servers) == 0 { server := spec.NewServer() p.openAPI.Servers = append(p.openAPI.Servers, server) } p.openAPI.Servers[0].Spec.URL += value println("@basepath is deprecated use servers instead") case acceptAttr: println("acceptAttribute is deprecated, as there is no such field on top level in spec V3.1") case produceAttr: println("produce is deprecated, as there is no such field on top level in spec V3.1") case "@schemes": println("@schemes is deprecated use servers instead") case "@tag.name": tag := &spec.Extendable[spec.Tag]{ Spec: &spec.Tag{ Name: value, }, } p.openAPI.Tags = append(p.openAPI.Tags, tag) case "@tag.description": tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] tag.Spec.Description = value case "@tag.description.markdown": tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] commentInfo, err := getMarkdownForTag(tag.Spec.Name, p.markdownFileDir) if err != nil { return err } tag.Spec.Description = string(commentInfo) case "@tag.docs.url": tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] tag.Spec.ExternalDocs = spec.NewExternalDocs() tag.Spec.ExternalDocs.Spec.URL = value case "@tag.docs.description": tag := p.openAPI.Tags[len(p.openAPI.Tags)-1] if tag.Spec.ExternalDocs == nil { return fmt.Errorf("%s needs to come after a @tags.docs.url", attribute) } tag.Spec.ExternalDocs.Spec.Description = value case secBasicAttr, secAPIKeyAttr, secApplicationAttr, secImplicitAttr, secPasswordAttr, secAccessCodeAttr, secBearerAuthAttr: key, scheme, err := parseSecAttributesV3(attribute, comments, &line) if err != nil { return err } schemeSpec := spec.NewSecuritySchemeSpec() schemeSpec.Spec.Spec = scheme if p.openAPI.Components.Spec.SecuritySchemes == nil { p.openAPI.Components.Spec.SecuritySchemes = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.SecurityScheme]]) } p.openAPI.Components.Spec.SecuritySchemes[key] = schemeSpec case "@query.collection.format": p.collectionFormatInQuery = TransToValidCollectionFormat(value) case extDocsDescAttr, extDocsURLAttr: if p.openAPI.ExternalDocs == nil { p.openAPI.ExternalDocs = spec.NewExternalDocs() } switch attr { case extDocsDescAttr: p.openAPI.ExternalDocs.Spec.Description = value case extDocsURLAttr: p.openAPI.ExternalDocs.Spec.URL = value } case "@x-taggroups": originalAttribute := strings.Split(commentLine, " ")[0] if len(value) == 0 { return fmt.Errorf("annotation %s need a value", attribute) } var valueJSON interface{} if err := json.Unmarshal([]byte(value), &valueJSON); err != nil { return fmt.Errorf("annotation %s need a valid json value. error: %s", originalAttribute, err.Error()) } p.openAPI.Info.Extensions[originalAttribute[1:]] = valueJSON case "@servers.url": server := spec.NewServer() server.Spec.URL = value matches := serversURLPattern.FindAllStringSubmatch(value, -1) server.Spec.Variables = make(map[string]*spec.Extendable[spec.ServerVariable]) for _, match := range matches { server.Spec.Variables[match[1]] = spec.NewServerVariable() } p.openAPI.Servers = append(p.openAPI.Servers, server) case "@servers.description": server := p.openAPI.Servers[len(p.openAPI.Servers)-1] server.Spec.Description = value case "@servers.variables.enum": server := p.openAPI.Servers[len(p.openAPI.Servers)-1] matches := serversVariablesPattern.FindStringSubmatch(value) if len(matches) > 0 { variable, ok := server.Spec.Variables[matches[1]] if !ok { p.debug.Printf("Variables are not detected.") continue } variable.Spec.Enum = append(variable.Spec.Enum, matches[2]) } case "@servers.variables.default": server := p.openAPI.Servers[len(p.openAPI.Servers)-1] matches := serversVariablesPattern.FindStringSubmatch(value) if len(matches) > 0 { variable, ok := server.Spec.Variables[matches[1]] if !ok { p.debug.Printf("Variables are not detected.") continue } variable.Spec.Default = matches[2] } case "@servers.variables.description": server := p.openAPI.Servers[len(p.openAPI.Servers)-1] matches := serversVariablesPattern.FindStringSubmatch(value) if len(matches) > 0 { variable, ok := server.Spec.Variables[matches[1]] if !ok { p.debug.Printf("Variables are not detected.") continue } variable.Spec.Default = matches[2] } case "@servers.variables.description.markdown": server := p.openAPI.Servers[len(p.openAPI.Servers)-1] matches := serversVariablesPattern.FindStringSubmatch(value) if len(matches) > 0 { variable, ok := server.Spec.Variables[matches[1]] if !ok { p.debug.Printf("Variables are not detected.") continue } commentInfo, err := getMarkdownForTag(matches[1], p.markdownFileDir) if err != nil { return err } variable.Spec.Description = string(commentInfo) } default: if strings.HasPrefix(attribute, "@x-") { err := p.parseExtensionsV3(value, attribute) if err != nil { return errors.Wrap(err, "could not parse extension comment") } } } previousAttribute = attribute } return nil } func (p *Parser) parseExtensionsV3(value, attribute string) error { extensionName := attribute[1:] // // for each security definition // for _, v := range p.openAPI.Components.Spec.SecuritySchemes{ // // check if extension exists // _, extExistsInSecurityDef := v.VendorExtensible.Extensions.GetString(extensionName) // // if it exists in at least one, then we stop iterating // if extExistsInSecurityDef { // return nil // } // } if len(value) == 0 { return fmt.Errorf("annotation %s need a value", attribute) } if p.openAPI.Info.Extensions == nil { p.openAPI.Info.Extensions = map[string]any{} } var valueJSON interface{} err := json.Unmarshal([]byte(value), &valueJSON) if err != nil { return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } if strings.Contains(extensionName, "logo") { p.openAPI.Info.Extensions[extensionName] = valueJSON return nil } p.openAPI.Info.Extensions[attribute[1:]] = valueJSON return nil } func setspecInfo(openAPI *spec.OpenAPI, attribute, value string) { switch attribute { case versionAttr: openAPI.Info.Spec.Version = value case titleAttr: openAPI.Info.Spec.Title = value case tosAttr: openAPI.Info.Spec.TermsOfService = value case descriptionAttr: openAPI.Info.Spec.Description = value case conNameAttr: if openAPI.Info.Spec.Contact == nil { openAPI.Info.Spec.Contact = spec.NewContact() } openAPI.Info.Spec.Contact.Spec.Name = value case conEmailAttr: if openAPI.Info.Spec.Contact == nil { openAPI.Info.Spec.Contact = spec.NewContact() } openAPI.Info.Spec.Contact.Spec.Email = value case conURLAttr: if openAPI.Info.Spec.Contact == nil { openAPI.Info.Spec.Contact = spec.NewContact() } openAPI.Info.Spec.Contact.Spec.URL = value case licNameAttr: if openAPI.Info.Spec.License == nil { openAPI.Info.Spec.License = spec.NewLicense() } openAPI.Info.Spec.License.Spec.Name = value case licURLAttr: if openAPI.Info.Spec.License == nil { openAPI.Info.Spec.License = spec.NewLicense() } openAPI.Info.Spec.License.Spec.URL = value } } func parseSecAttributesV3(context string, lines []string, index *int) (string, *spec.SecurityScheme, error) { const ( in = "@in" name = "@name" descriptionAttr = "@description" tokenURL = "@tokenurl" authorizationURL = "@authorizationurl" ) var search []string attribute := strings.ToLower(FieldsByAnySpace(lines[*index], 2)[0]) switch attribute { case secBasicAttr: scheme := spec.SecurityScheme{ Type: "http", Scheme: "basic", } return "basic", &scheme, nil case secAPIKeyAttr: search = []string{in, name} case secApplicationAttr, secPasswordAttr: search = []string{tokenURL, in, name} case secImplicitAttr: search = []string{authorizationURL, in} case secAccessCodeAttr: search = []string{tokenURL, authorizationURL, in} case secBearerAuthAttr: scheme := spec.SecurityScheme{ Type: "http", Scheme: "bearer", BearerFormat: "JWT", } return "bearerauth", &scheme, nil } // For the first line we get the attributes in the context parameter, so we skip to the next one *index++ attrMap, scopes := make(map[string]string), make(map[string]string) extensions, description := make(map[string]interface{}), "" for ; *index < len(lines); *index++ { v := strings.TrimSpace(lines[*index]) if len(v) == 0 { continue } fields := FieldsByAnySpace(v, 2) securityAttr := strings.ToLower(fields[0]) var value string if len(fields) > 1 { value = fields[1] } for _, findTerm := range search { if securityAttr == findTerm { attrMap[securityAttr] = value break } } isExists, err := isExistsScope(securityAttr) if err != nil { return "", nil, err } if isExists { scopes[securityAttr[len(scopeAttrPrefix):]] = v[len(securityAttr):] } if strings.HasPrefix(securityAttr, "@x-") { // Add the custom attribute without the @ extensions[securityAttr[1:]] = value } // Not mandatory field if securityAttr == descriptionAttr { description = value } // next securityDefinitions if strings.Index(securityAttr, "@securitydefinitions.") == 0 { // Go back to the previous line and break *index-- break } } if len(attrMap) != len(search) { return "", nil, fmt.Errorf("%s is %v required", context, search) } scheme := &spec.SecurityScheme{} key := getSecurityDefinitionKey(lines) switch attribute { case secAPIKeyAttr: scheme.Type = "apiKey" scheme.In = attrMap[in] scheme.Name = attrMap[name] case secApplicationAttr: scheme.Type = "oauth2" scheme.In = attrMap[in] scheme.Flows = spec.NewOAuthFlows() scheme.Flows.Spec.ClientCredentials = spec.NewOAuthFlow() scheme.Flows.Spec.ClientCredentials.Spec.TokenURL = attrMap[tokenURL] scheme.Flows.Spec.ClientCredentials.Spec.Scopes = make(map[string]string) for k, v := range scopes { scheme.Flows.Spec.ClientCredentials.Spec.Scopes[k] = v } case secImplicitAttr: scheme.Type = "oauth2" scheme.In = attrMap[in] scheme.Flows = spec.NewOAuthFlows() scheme.Flows.Spec.Implicit = spec.NewOAuthFlow() scheme.Flows.Spec.Implicit.Spec.AuthorizationURL = attrMap[authorizationURL] scheme.Flows.Spec.Implicit.Spec.Scopes = make(map[string]string) for k, v := range scopes { scheme.Flows.Spec.Implicit.Spec.Scopes[k] = v } case secPasswordAttr: scheme.Type = "oauth2" scheme.In = attrMap[in] scheme.Flows = spec.NewOAuthFlows() scheme.Flows.Spec.Password = spec.NewOAuthFlow() scheme.Flows.Spec.Password.Spec.TokenURL = attrMap[tokenURL] scheme.Flows.Spec.Password.Spec.Scopes = make(map[string]string) for k, v := range scopes { scheme.Flows.Spec.Password.Spec.Scopes[k] = v } case secAccessCodeAttr: scheme.Type = "oauth2" scheme.In = attrMap[in] scheme.Flows = spec.NewOAuthFlows() scheme.Flows.Spec.AuthorizationCode = spec.NewOAuthFlow() scheme.Flows.Spec.AuthorizationCode.Spec.AuthorizationURL = attrMap[authorizationURL] scheme.Flows.Spec.AuthorizationCode.Spec.TokenURL = attrMap[tokenURL] } scheme.Description = description if scheme.Flows != nil && scheme.Flows.Extensions == nil && len(extensions) > 0 { scheme.Flows.Extensions = make(map[string]interface{}) } for k, v := range extensions { scheme.Flows.Extensions[k] = v } return key, scheme, nil } func getSecurityDefinitionKey(lines []string) string { for _, line := range lines { if strings.HasPrefix(strings.ToLower(line), "@securitydefinitions") { splittedLine := strings.Split(line, " ") return splittedLine[len(splittedLine)-1] } } return "" } // ParseRouterAPIInfoV3 parses router api info for given astFile. func (p *Parser) ParseRouterAPIInfoV3(fileInfo *AstFileInfo) error { for _, astDescription := range fileInfo.File.Decls { if (fileInfo.ParseFlag & ParseOperations) == ParseNone { continue } astDeclaration, ok := astDescription.(*ast.FuncDecl) if !ok || astDeclaration.Doc == nil || astDeclaration.Doc.List == nil { continue } if p.matchTags(astDeclaration.Doc.List) && matchExtension(p.parseExtension, astDeclaration.Doc.List) { // for per 'function' comment, create a new 'Operation' object operation := NewOperationV3(p, SetCodeExampleFilesDirectoryV3(p.codeExampleFilesDir)) for _, comment := range astDeclaration.Doc.List { err := operation.ParseComment(comment.Text, fileInfo.File) if err != nil { return fmt.Errorf("ParseComment error in file %s :%+v", fileInfo.Path, err) } } // workaround until we replace the produce comment with a new @Success syntax // We first need to setup all responses before we can set the mimetypes err := operation.ProcessProduceComment() if err != nil { return err } err = processRouterOperationV3(p, operation) if err != nil { return err } } } return nil } func processRouterOperationV3(p *Parser, o *OperationV3) error { for _, routeProperties := range o.RouterProperties { var ( pathItem *spec.RefOrSpec[spec.Extendable[spec.PathItem]] ok bool ) pathItem, ok = p.openAPI.Paths.Spec.Paths[routeProperties.Path] if !ok { pathItem = &spec.RefOrSpec[spec.Extendable[spec.PathItem]]{ Spec: &spec.Extendable[spec.PathItem]{ Spec: &spec.PathItem{}, }, } } op := refRouteMethodOpV3(pathItem.Spec.Spec, routeProperties.HTTPMethod) // check if we already have an operation for this path and method if *op != nil { err := fmt.Errorf("route %s %s is declared multiple times", routeProperties.HTTPMethod, routeProperties.Path) if p.Strict { return err } p.debug.Printf("warning: %s\n", err) } *op = &o.Operation p.openAPI.Paths.Spec.Paths[routeProperties.Path] = pathItem } return nil } func refRouteMethodOpV3(item *spec.PathItem, method string) **spec.Operation { switch method { case http.MethodGet: if item.Get == nil { item.Get = &spec.Extendable[spec.Operation]{} } return &item.Get.Spec case http.MethodPost: if item.Post == nil { item.Post = &spec.Extendable[spec.Operation]{} } return &item.Post.Spec case http.MethodDelete: if item.Delete == nil { item.Delete = &spec.Extendable[spec.Operation]{} } return &item.Delete.Spec case http.MethodPut: if item.Put == nil { item.Put = &spec.Extendable[spec.Operation]{} } return &item.Put.Spec case http.MethodPatch: if item.Patch == nil { item.Patch = &spec.Extendable[spec.Operation]{} } return &item.Patch.Spec case http.MethodHead: if item.Head == nil { item.Head = &spec.Extendable[spec.Operation]{} } return &item.Head.Spec case http.MethodOptions: if item.Options == nil { item.Options = &spec.Extendable[spec.Operation]{} } return &item.Options.Spec default: return nil } } func (p *Parser) getTypeSchemaV3(typeName string, file *ast.File, ref bool) (*spec.RefOrSpec[spec.Schema], error) { if override, ok := p.Overrides[typeName]; ok { p.debug.Printf("Override detected for %s: using %s instead", typeName, override) schema, err := parseObjectSchemaV3(p, override, file) if err != nil { return nil, err } return schema, nil } if IsInterfaceLike(typeName) { return spec.NewSchemaSpec(), nil } if IsGolangPrimitiveType(typeName) { return PrimitiveSchemaV3(TransToValidSchemeType(typeName)), nil } schemaType, err := convertFromSpecificToPrimitive(typeName) if err == nil { return PrimitiveSchemaV3(schemaType), nil } typeSpecDef := p.packages.FindTypeSpec(typeName, file) if typeSpecDef == nil { p.packages.FindTypeSpec(typeName, file) // uncomment for debugging return nil, fmt.Errorf("cannot find type definition: %s", typeName) } if override, ok := p.Overrides[typeSpecDef.FullPath()]; ok { if override == "" { p.debug.Printf("Override detected for %s: ignoring", typeSpecDef.FullPath()) return nil, ErrSkippedField } p.debug.Printf("Override detected for %s: using %s instead", typeSpecDef.FullPath(), override) separator := strings.LastIndex(override, ".") if separator == -1 { // treat as a swaggertype tag parts := strings.Split(override, ",") return BuildCustomSchemaV3(parts) } typeSpecDef = p.packages.findTypeSpec(override[0:separator], override[separator+1:]) } schema, ok := p.parsedSchemasV3[typeSpecDef] if !ok { var err error schema, err = p.ParseDefinitionV3(typeSpecDef) if err != nil { if err == ErrRecursiveParseStruct && ref { return p.getRefTypeSchemaV3(typeSpecDef, schema), nil } return nil, err } } if ref { if IsComplexSchemaV3(schema) { return p.getRefTypeSchemaV3(typeSpecDef, schema), nil } // if it is a simple schema, just return a copy newSchema := *schema.Schema return spec.NewRefOrSpec(nil, &newSchema), nil } return spec.NewRefOrSpec(nil, schema.Schema), nil } // ParseDefinitionV3 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 (p *Parser) ParseDefinitionV3(typeSpecDef *TypeSpecDef) (*SchemaV3, error) { typeName := typeSpecDef.TypeName() schema, found := p.parsedSchemasV3[typeSpecDef] if found { p.debug.Printf("Skipping '%s', already parsed.", typeName) return schema, nil } if p.isInStructStack(typeSpecDef) { p.debug.Printf("Skipping '%s', recursion detected.", typeName) return &SchemaV3{ Name: typeName, PkgPath: typeSpecDef.PkgPath, Schema: PrimitiveSchemaV3(OBJECT).Spec, }, ErrRecursiveParseStruct } p.structStack = append(p.structStack, typeSpecDef) p.debug.Printf("Generating %s", typeName) definition, err := p.parseTypeExprV3(typeSpecDef.File, typeSpecDef.TypeSpec.Type, false) if err != nil { p.debug.Printf("Error parsing type definition '%s': %s", typeName, err) return nil, err } if definition.Spec.Description == "" { fillDefinitionDescriptionV3(p, definition.Spec, typeSpecDef.File, typeSpecDef) } if len(typeSpecDef.Enums) > 0 { var varNames []string var enumComments = make(map[string]string) for _, value := range typeSpecDef.Enums { definition.Spec.Enum = append(definition.Spec.Enum, value.Value) varNames = append(varNames, value.key) if len(value.Comment) > 0 { enumComments[value.key] = value.Comment } } if definition.Spec.Extensions == nil { definition.Spec.Extensions = make(map[string]any) } definition.Spec.Extensions[enumVarNamesExtension] = varNames if len(enumComments) > 0 { definition.Spec.Extensions[enumCommentsExtension] = enumComments } } sch := SchemaV3{ Name: typeName, PkgPath: typeSpecDef.PkgPath, Schema: definition.Spec, } p.parsedSchemasV3[typeSpecDef] = &sch // update an empty schema as a result of recursion s2, found := p.outputSchemasV3[typeSpecDef] if found { p.openAPI.Components.Spec.Schemas[s2.Name] = definition } return &sch, nil } // fillDefinitionDescription additionally fills fields in definition (spec.Schema) // TODO: If .go file contains many types, it may work for a long time func fillDefinitionDescriptionV3(parser *Parser, definition *spec.Schema, file *ast.File, typeSpecDef *TypeSpecDef) { for _, astDeclaration := range file.Decls { generalDeclaration, ok := astDeclaration.(*ast.GenDecl) if !ok || generalDeclaration.Tok != token.TYPE { continue } for _, astSpec := range generalDeclaration.Specs { typeSpec, ok := astSpec.(*ast.TypeSpec) if !ok || typeSpec != typeSpecDef.TypeSpec { continue } var typeName string if typeSpec.Name != nil { typeName = typeSpec.Name.Name } text, err := parser.extractDeclarationDescription(typeName, typeSpec.Comment, generalDeclaration.Doc) if err != nil { parser.debug.Printf("Error extracting declaration description: %s", err) continue } definition.Description = text } } } // parseTypeExprV3 parses given type expression that corresponds to the type under // given name and package, and returns swagger schema for it. func (p *Parser) parseTypeExprV3(file *ast.File, typeExpr ast.Expr, ref bool) (*spec.RefOrSpec[spec.Schema], error) { const errMessage = "parse type expression v3" switch expr := typeExpr.(type) { // type Foo interface{} case *ast.InterfaceType: return spec.NewSchemaSpec(), nil // type Foo struct {...} case *ast.StructType: return p.parseStructV3(file, expr.Fields) // type Foo Baz case *ast.Ident: result, err := p.getTypeSchemaV3(expr.Name, file, ref) if err != nil { return nil, errors.Wrap(err, errMessage) } return result, nil // type Foo *Baz case *ast.StarExpr: return p.parseTypeExprV3(file, expr.X, ref) // type Foo pkg.Bar case *ast.SelectorExpr: if xIdent, ok := expr.X.(*ast.Ident); ok { result, err := p.getTypeSchemaV3(fullTypeName(xIdent.Name, expr.Sel.Name), file, ref) if err != nil { return nil, errors.Wrap(err, errMessage) } return result, nil } // type Foo []Baz case *ast.ArrayType: itemSchema, err := p.parseTypeExprV3(file, expr.Elt, true) if err != nil { return nil, err } if itemSchema == nil { schema := &spec.Schema{} schema.Type = spec.NewSingleOrArray(ARRAY) schema.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) p.debug.Printf("Creating array with empty item schema %v", expr.Elt) return spec.NewRefOrSpec(nil, schema), nil } result := &spec.Schema{} result.Type = spec.NewSingleOrArray(ARRAY) result.Items = spec.NewBoolOrSchema(false, itemSchema) return spec.NewRefOrSpec(nil, result), nil // type Foo map[string]Bar case *ast.MapType: if _, ok := expr.Value.(*ast.InterfaceType); ok { result := &spec.Schema{} result.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) result.Type = spec.NewSingleOrArray(OBJECT) return spec.NewRefOrSpec(nil, result), nil } schema, err := p.parseTypeExprV3(file, expr.Value, true) if err != nil { return nil, err } result := &spec.Schema{} result.AdditionalProperties = spec.NewBoolOrSchema(false, schema) result.Type = spec.NewSingleOrArray(OBJECT) return spec.NewRefOrSpec(nil, result), nil case *ast.FuncType: return nil, ErrFuncTypeField // ... } return p.parseGenericTypeExprV3(file, typeExpr) } func (p *Parser) parseStructV3(file *ast.File, fields *ast.FieldList) (*spec.RefOrSpec[spec.Schema], error) { required, properties := make([]string, 0), make(map[string]*spec.RefOrSpec[spec.Schema]) for _, field := range fields.List { fieldProps, requiredFromAnon, err := p.parseStructFieldV3(file, field) if err != nil { if err == ErrFuncTypeField || err == ErrSkippedField { continue } return nil, err } if len(fieldProps) == 0 { continue } required = append(required, requiredFromAnon...) for k, v := range fieldProps { properties[k] = v } } sort.Strings(required) result := spec.NewSchemaSpec() result.Spec.Type = spec.NewSingleOrArray(OBJECT) result.Spec.Properties = properties result.Spec.Required = required return result, nil } func (p *Parser) parseStructFieldV3(file *ast.File, field *ast.Field) (map[string]*spec.RefOrSpec[spec.Schema], []string, error) { if field.Tag != nil { skip, ok := reflect.StructTag(strings.ReplaceAll(field.Tag.Value, "`", "")).Lookup("swaggerignore") if ok && strings.EqualFold(skip, "true") { return nil, nil, nil } } ps := p.fieldParserFactoryV3(p, file, field) if ps.ShouldSkip() { return nil, nil, nil } fieldName, err := ps.FieldName() if err != nil { return nil, nil, err } if fieldName == "" { typeName, err := getFieldType(file, field.Type, nil) if err != nil { return nil, nil, err } schema, err := p.getTypeSchemaV3(typeName, file, false) if err != nil { return nil, nil, err } if len(schema.Spec.Type) > 0 && schema.Spec.Type[0] == OBJECT { if len(schema.Spec.Properties) == 0 { return nil, nil, nil } properties := make(map[string]*spec.RefOrSpec[spec.Schema]) for k, v := range schema.Spec.Properties { properties[k] = v } return properties, schema.Spec.Required, nil } // for alias type of non-struct types ,such as array,map, etc. ignore field tag. return map[string]*spec.RefOrSpec[spec.Schema]{ typeName: schema, }, nil, nil } schema, err := ps.CustomSchema() if err != nil { return nil, nil, err } if schema == nil { typeName, err := getFieldType(file, field.Type, nil) if err == nil { // named type schema, err = p.getTypeSchemaV3(typeName, file, true) if err != nil { return nil, nil, err } } else { // unnamed type parsedSchema, err := p.parseTypeExprV3(file, field.Type, false) if err != nil { return nil, nil, err } schema = parsedSchema } } err = ps.ComplementSchema(schema) if err != nil { return nil, nil, err } var tagRequired []string required, err := ps.IsRequired() if err != nil { return nil, nil, err } if required { tagRequired = append(tagRequired, fieldName) } if formName := ps.FormName(); len(formName) > 0 { if schema.Spec.Extensions == nil { schema.Spec.Extensions = make(map[string]any) } schema.Spec.Extensions[formTag] = formName } return map[string]*spec.RefOrSpec[spec.Schema]{fieldName: schema}, tagRequired, nil } func (p *Parser) getRefTypeSchemaV3(typeSpecDef *TypeSpecDef, schema *SchemaV3) *spec.RefOrSpec[spec.Schema] { _, ok := p.outputSchemasV3[typeSpecDef] if !ok { if p.openAPI.Components.Spec.Schemas == nil { p.openAPI.Components.Spec.Schemas = make(map[string]*spec.RefOrSpec[spec.Schema]) } p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewSchemaSpec() if schema.Schema != nil { p.openAPI.Components.Spec.Schemas[schema.Name] = spec.NewRefOrSpec(nil, schema.Schema) } p.outputSchemasV3[typeSpecDef] = schema } refSchema := RefSchemaV3(schema.Name) return refSchema } // GetSchemaTypePathV3 get path of schema type. func (p *Parser) GetSchemaTypePathV3(schema *spec.RefOrSpec[spec.Schema], depth int) []string { if schema == nil || depth == 0 { return nil } name := "" if schema.Ref != nil { name = schema.Ref.Ref } if name != "" { if pos := strings.LastIndexByte(name, '/'); pos >= 0 { name = name[pos+1:] if schema, ok := p.openAPI.Components.Spec.Schemas[name]; ok { return p.GetSchemaTypePathV3(schema, depth) } } return nil } if schema.Spec != nil && len(schema.Spec.Type) > 0 { switch schema.Spec.Type[0] { case ARRAY: depth-- s := []string{schema.Spec.Type[0]} return append(s, p.GetSchemaTypePathV3(schema.Spec.Items.Schema, depth)...) case OBJECT: if schema.Spec.AdditionalProperties != nil && schema.Spec.AdditionalProperties.Schema != nil { // for map depth-- s := []string{schema.Spec.Type[0]} return append(s, p.GetSchemaTypePathV3(schema.Spec.AdditionalProperties.Schema, depth)...) } } return []string{schema.Spec.Type[0]} } println("found schema with no Type, returning any") return []string{ANY} } func (p *Parser) getSchemaByRef(ref *spec.Ref) *spec.Schema { searchString := strings.ReplaceAll(ref.Ref, "#/components/schemas/", "") return p.openAPI.Components.Spec.Schemas[searchString].Spec }