package swag import ( "encoding/json" "fmt" "go/ast" "log" "net/http" "strconv" "strings" "github.com/pkg/errors" "github.com/sv-tools/openapi/spec" "gopkg.in/yaml.v2" ) // OperationV3 describes a single API operation on a path. // For more information: https://github.com/swaggo/swag#api-operation type OperationV3 struct { parser *Parser codeExampleFilesDir string spec.Operation RouterProperties []RouteProperties responseMimeTypes []string } // NewOperationV3 returns a new instance of OperationV3. func NewOperationV3(parser *Parser, options ...func(*OperationV3)) *OperationV3 { op := *spec.NewOperation().Spec op.Responses = spec.NewResponses() operation := &OperationV3{ parser: parser, Operation: op, } for _, option := range options { option(operation) } return operation } // SetCodeExampleFilesDirectoryV3 sets the directory to search for codeExamples. func SetCodeExampleFilesDirectoryV3(directoryPath string) func(*OperationV3) { return func(o *OperationV3) { o.codeExampleFilesDir = directoryPath } } // ParseComment parses comment for given comment string and returns error if error occurs. func (o *OperationV3) ParseComment(comment string, astFile *ast.File) error { commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/")) if len(commentLine) == 0 { return nil } fields := FieldsByAnySpace(commentLine, 2) attribute := fields[0] lowerAttribute := strings.ToLower(attribute) var lineRemainder string if len(fields) > 1 { lineRemainder = fields[1] } switch lowerAttribute { case descriptionAttr: o.ParseDescriptionComment(lineRemainder) case descriptionMarkdownAttr: commentInfo, err := getMarkdownForTag(lineRemainder, o.parser.markdownFileDir) if err != nil { return err } o.ParseDescriptionComment(string(commentInfo)) case summaryAttr: o.Summary = lineRemainder case idAttr: o.OperationID = lineRemainder case tagsAttr: o.ParseTagsComment(lineRemainder) case acceptAttr: return o.ParseAcceptComment(lineRemainder) case produceAttr: return o.ParseProduceComment(lineRemainder) case paramAttr: return o.ParseParamComment(lineRemainder, astFile) case successAttr, failureAttr, responseAttr: return o.ParseResponseComment(lineRemainder, astFile) case headerAttr: return o.ParseResponseHeaderComment(lineRemainder, astFile) case routerAttr: return o.ParseRouterComment(lineRemainder) case securityAttr: return o.ParseSecurityComment(lineRemainder) case deprecatedAttr: o.Deprecated = true case xCodeSamplesAttr, xCodeSamplesAttrOriginal: return o.ParseCodeSample(attribute, commentLine, lineRemainder) case "@servers.url": return o.ParseServerURLComment(lineRemainder) case "@servers.description": return o.ParseServerDescriptionComment(lineRemainder) default: return o.ParseMetadata(attribute, lowerAttribute, lineRemainder) } return nil } // ParseDescriptionComment parses the description comment and sets it to the operation. func (o *OperationV3) ParseDescriptionComment(lineRemainder string) { if o.Description == "" { o.Description = lineRemainder return } o.Description += "\n" + lineRemainder } // ParseMetadata godoc. func (o *OperationV3) ParseMetadata(attribute, lowerAttribute, lineRemainder string) error { // parsing specific meta data extensions if strings.HasPrefix(lowerAttribute, "@x-") { if len(lineRemainder) == 0 { return fmt.Errorf("annotation %s need a value", attribute) } var valueJSON any err := json.Unmarshal([]byte(lineRemainder), &valueJSON) if err != nil { return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } o.Responses.Extensions[attribute[1:]] = valueJSON return nil } return nil } // ParseTagsComment parses comment for given `tag` comment string. func (o *OperationV3) ParseTagsComment(commentLine string) { for _, tag := range strings.Split(commentLine, ",") { o.Tags = append(o.Tags, strings.TrimSpace(tag)) } } // ParseAcceptComment parses comment for given `accept` comment string. func (o *OperationV3) ParseAcceptComment(commentLine string) error { const errMessage = "could not parse accept comment" validTypes, err := parseMimeTypeListV3(commentLine, "%v accept type can't be accepted") if err != nil { return errors.Wrap(err, errMessage) } if o.RequestBody == nil { o.RequestBody = spec.NewRequestBodySpec() } if o.RequestBody.Spec.Spec.Content == nil { o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType], len(validTypes)) } for _, value := range validTypes { // skip correctly setup types like application/json if o.RequestBody.Spec.Spec.Content[value] != nil { continue } mediaType := spec.NewMediaType() schema := spec.NewSchemaSpec() switch value { case "application/json", "multipart/form-data", "text/xml": schema.Spec.Type = spec.NewSingleOrArray(OBJECT) case "image/png", "image/jpeg", "image/gif", "application/octet-stream", "application/pdf", "application/msexcel", "application/zip", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation": schema.Spec.Type = spec.NewSingleOrArray(STRING) schema.Spec.Format = "binary" default: schema.Spec.Type = spec.NewSingleOrArray(STRING) } mediaType.Spec.Schema = schema o.RequestBody.Spec.Spec.Content[value] = mediaType } return nil } // ParseProduceComment parses comment for given `produce` comment string. func (o *OperationV3) ParseProduceComment(commentLine string) error { const errMessage = "could not parse produce comment" validTypes, err := parseMimeTypeListV3(commentLine, "%v produce type can't be accepted") if err != nil { return errors.Wrap(err, errMessage) } o.responseMimeTypes = validTypes return nil } // ProcessProduceComment processes the previously parsed produce comment. func (o *OperationV3) ProcessProduceComment() error { const errMessage = "could not process produce comment" if o.Responses == nil { return nil } for _, value := range o.responseMimeTypes { if o.Responses.Spec.Response == nil { o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]], len(o.responseMimeTypes)) } for key, response := range o.Responses.Spec.Response { code, err := strconv.Atoi(key) if err != nil { return errors.Wrap(err, errMessage) } // Status 204 is no content. So we do not need to add content. if code == 204 { continue } // As this is a workaround, we need to check if the code is in range. // The Produce comment is being deprecated soon. if code < 200 || code > 299 { continue } // skip correctly setup types like application/json if response.Spec.Spec.Content[value] != nil { continue } mediaType := spec.NewMediaType() schema := spec.NewSchemaSpec() switch value { case "application/json", "multipart/form-data", "text/xml": schema.Spec.Type = spec.NewSingleOrArray(OBJECT) case "image/png", "image/jpeg", "image/gif", "application/octet-stream", "application/pdf", "application/msexcel", "application/zip", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation": schema.Spec.Type = spec.NewSingleOrArray(STRING) schema.Spec.Format = "binary" default: schema.Spec.Type = spec.NewSingleOrArray(STRING) } mediaType.Spec.Schema = schema if response.Spec.Spec.Content == nil { response.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) } response.Spec.Spec.Content[value] = mediaType } } return nil } // parseMimeTypeList parses a list of MIME Types for a comment like // `produce` (`Content-Type:` response header) or // `accept` (`Accept:` request header). func parseMimeTypeListV3(mimeTypeList string, format string) ([]string, error) { var result []string for _, typeName := range strings.Split(mimeTypeList, ",") { typeName = strings.TrimSpace(typeName) if mimeTypePattern.MatchString(typeName) { result = append(result, typeName) continue } aliasMimeType, ok := mimeTypeAliases[typeName] if !ok { return nil, fmt.Errorf(format, typeName) } result = append(result, aliasMimeType) } return result, nil } // ParseParamComment parses params return []string of param properties // E.g. @Param queryText formData string true "The email for login" // // [param name] [paramType] [data type] [is mandatory?] [Comment] // // E.g. @Param some_id path int true "Some ID". func (o *OperationV3) ParseParamComment(commentLine string, astFile *ast.File) error { matches := paramPattern.FindStringSubmatch(commentLine) if len(matches) != 6 { return fmt.Errorf("missing required param comment parameters \"%s\"", commentLine) } name := matches[1] paramType := matches[2] refType := TransToValidSchemeType(matches[3]) // Detect refType objectType := OBJECT if strings.HasPrefix(refType, "[]") { objectType = ARRAY refType = strings.TrimPrefix(refType, "[]") refType = TransToValidSchemeType(refType) } else if IsPrimitiveType(refType) || paramType == "formData" && refType == "file" { objectType = PRIMITIVE } var enums []interface{} if !IsPrimitiveType(refType) { schema, _ := o.parser.getTypeSchemaV3(refType, astFile, false) if schema != nil && schema.Spec != nil && schema.Spec.Enum != nil { // schema.Spec.Type != ARRAY fmt.Println(schema.Spec.Type) if objectType == OBJECT { objectType = PRIMITIVE } refType = TransToValidSchemeType(schema.Spec.Type[0]) enums = schema.Spec.Enum } } requiredText := strings.ToLower(matches[4]) required := requiredText == "true" || requiredText == requiredLabel description := matches[5] param := createParameterV3(paramType, description, name, objectType, refType, required, enums, o.parser.collectionFormatInQuery) switch paramType { case "path", "header": switch objectType { case ARRAY: if !IsPrimitiveType(refType) { return fmt.Errorf("%s is not supported array type for %s", refType, paramType) } case OBJECT: return fmt.Errorf("%s is not supported type for %s", refType, paramType) } case "query": switch objectType { case ARRAY: if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") { return fmt.Errorf("%s is not supported array type for %s", refType, paramType) } case PRIMITIVE: break case OBJECT: schema, err := o.parser.getTypeSchemaV3(refType, astFile, false) if err != nil { return err } if len(schema.Spec.Properties) == 0 { return nil } for name, item := range schema.Spec.Properties { prop := item.Spec if len(prop.Type) == 0 { continue } itemParam := param // Avoid shadowed variable which could cause side effects to o.Operation.Parameters switch { case prop.Type[0] == ARRAY && prop.Items.Schema != nil && len(prop.Items.Schema.Spec.Type) > 0 && IsSimplePrimitiveType(prop.Items.Schema.Spec.Type[0]): itemParam = createParameterV3(paramType, prop.Description, name, prop.Type[0], prop.Items.Schema.Spec.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) case IsSimplePrimitiveType(prop.Type[0]): itemParam = createParameterV3(paramType, prop.Description, name, PRIMITIVE, prop.Type[0], findInSlice(schema.Spec.Required, name), enums, o.parser.collectionFormatInQuery) default: o.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType) continue } itemParam.Schema.Spec = prop listItem := &spec.RefOrSpec[spec.Extendable[spec.Parameter]]{ Spec: &spec.Extendable[spec.Parameter]{ Spec: &itemParam, }, } o.Operation.Parameters = append(o.Operation.Parameters, listItem) } return nil } case "body", "formData": if objectType == PRIMITIVE { schema := PrimitiveSchemaV3(refType) err := o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) if err != nil { return err } o.fillRequestBody(schema, required, description, true, paramType == "formData") return nil } schema, err := o.parseAPIObjectSchema(commentLine, objectType, refType, astFile) if err != nil { return err } err = o.parseParamAttributeForBody(commentLine, objectType, refType, schema.Spec) if err != nil { return err } o.fillRequestBody(schema, required, description, false, paramType == "formData") return nil default: return fmt.Errorf("%s is not supported paramType", paramType) } err := o.parseParamAttribute(commentLine, objectType, refType, ¶m) if err != nil { return err } item := spec.NewRefOrSpec(nil, &spec.Extendable[spec.Parameter]{ Spec: ¶m, }) o.Operation.Parameters = append(o.Operation.Parameters, item) return nil } func (o *OperationV3) fillRequestBody(schema *spec.RefOrSpec[spec.Schema], required bool, description string, primitive, formData bool) { if o.RequestBody == nil { o.RequestBody = spec.NewRequestBodySpec() o.RequestBody.Spec.Spec.Content = make(map[string]*spec.Extendable[spec.MediaType]) if primitive && !formData { o.RequestBody.Spec.Spec.Content["text/plain"] = spec.NewMediaType() } else if formData { o.RequestBody.Spec.Spec.Content["application/x-www-form-urlencoded"] = spec.NewMediaType() } else { o.RequestBody.Spec.Spec.Content["application/json"] = spec.NewMediaType() } } o.RequestBody.Spec.Spec.Description = description o.RequestBody.Spec.Spec.Required = required for _, value := range o.RequestBody.Spec.Spec.Content { value.Spec.Schema = schema } } func (o *OperationV3) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error { if param == nil { return fmt.Errorf("cannot parse empty parameter for comment: %s", comment) } schemaType = TransToValidSchemeType(schemaType) for attrKey, re := range regexAttributes { attr, err := findAttr(re, comment) if err != nil { continue } switch attrKey { case enumsTag: err = setEnumParamV3(param.Schema.Spec, attr, objectType, schemaType) case minimumTag, maximumTag: err = setNumberParamV3(param.Schema.Spec, attrKey, schemaType, attr, comment) case defaultTag: err = setDefaultV3(param.Schema.Spec, schemaType, attr) case minLengthTag, maxLengthTag: err = setStringParamV3(param.Schema.Spec, attrKey, schemaType, attr, comment) case formatTag: param.Schema.Spec.Format = attr case exampleTag: val, err := defineType(schemaType, attr) if err != nil { continue // Don't set a example value if it's not valid } param.Example = val case schemaExampleTag: err = setSchemaExampleV3(param.Schema.Spec, schemaType, attr) case extensionsTag: param.Schema.Spec.Extensions = setExtensionParam(attr) case collectionFormatTag: err = setCollectionFormatParamV3(param, attrKey, objectType, attr, comment) } if err != nil { return err } } return nil } func (o *OperationV3) parseParamAttributeForBody(comment, objectType, schemaType string, param *spec.Schema) error { schemaType = TransToValidSchemeType(schemaType) for attrKey, re := range regexAttributes { attr, err := findAttr(re, comment) if err != nil { continue } switch attrKey { case enumsTag: err = setEnumParamV3(param, attr, objectType, schemaType) case minimumTag, maximumTag: err = setNumberParamV3(param, attrKey, schemaType, attr, comment) case defaultTag: err = setDefaultV3(param, schemaType, attr) case minLengthTag, maxLengthTag: err = setStringParamV3(param, attrKey, schemaType, attr, comment) case formatTag: param.Format = attr case exampleTag: err = setSchemaExampleV3(param, schemaType, attr) case schemaExampleTag: err = setSchemaExampleV3(param, schemaType, attr) case extensionsTag: param.Extensions = setExtensionParam(attr) } if err != nil { return err } } return nil } func setCollectionFormatParamV3(param *spec.Parameter, name, schemaType, attr, commentLine string) error { if schemaType == ARRAY { param.Style = TransToValidCollectionFormatV3(attr, param.In) return nil } return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType) } func setSchemaExampleV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a example value if it's not valid } // skip schema if param == nil { return nil } switch v := val.(type) { case string: // replaces \r \n \t in example string values. param.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v) default: param.Example = val } return nil } func setExampleParameterV3(param *spec.Parameter, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a example value if it's not valid } param.Example = val return nil } func setStringParamV3(param *spec.Schema, name, schemaType, attr, commentLine string) error { if schemaType != STRING { return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } n, err := strconv.Atoi(attr) if err != nil { return fmt.Errorf("%s is allow only a number got=%s", name, attr) } switch name { case minLengthTag: param.MinLength = &n case maxLengthTag: param.MaxLength = &n } return nil } func setDefaultV3(param *spec.Schema, schemaType string, value string) error { val, err := defineType(schemaType, value) if err != nil { return nil // Don't set a default value if it's not valid } param.Default = val return nil } func setEnumParamV3(param *spec.Schema, attr, objectType, schemaType string) error { for _, e := range strings.Split(attr, ",") { e = strings.TrimSpace(e) value, err := defineType(schemaType, e) if err != nil { return err } switch objectType { case ARRAY: param.Items.Schema.Spec.Enum = append(param.Items.Schema.Spec.Enum, value) default: param.Enum = append(param.Enum, value) } } return nil } func setNumberParamV3(param *spec.Schema, name, schemaType, attr, commentLine string) error { switch schemaType { case INTEGER, NUMBER: n, err := strconv.Atoi(attr) if err != nil { return fmt.Errorf("maximum is allow only a number. comment=%s got=%s", commentLine, attr) } switch name { case minimumTag: param.Minimum = &n case maximumTag: param.Maximum = &n } return nil default: return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType) } } func (o *OperationV3) parseAPIObjectSchema(commentLine, schemaType, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { if strings.HasSuffix(refType, ",") && strings.Contains(refType, "[") { // regexp may have broken generic syntax. find closing bracket and add it back allMatchesLenOffset := strings.Index(commentLine, refType) + len(refType) lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]") if lostPartEndIdx >= 0 { refType += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1] } } switch schemaType { case OBJECT: if !strings.HasPrefix(refType, "[]") { return o.parseObjectSchema(refType, astFile) } refType = refType[2:] fallthrough case ARRAY: schema, err := o.parseObjectSchema(refType, astFile) if err != nil { return nil, err } result := spec.NewSchemaSpec() result.Spec.Type = spec.NewSingleOrArray("array") result.Spec.Items = spec.NewBoolOrSchema(false, schema) // TODO: allowed? return result, nil default: return PrimitiveSchemaV3(schemaType), nil } } // ParseRouterComment parses comment for given `router` comment string. func (o *OperationV3) ParseRouterComment(commentLine string) error { matches := routerPattern.FindStringSubmatch(commentLine) if len(matches) != 3 { return fmt.Errorf("can not parse router comment \"%s\"", commentLine) } signature := RouteProperties{ Path: matches[1], HTTPMethod: strings.ToUpper(matches[2]), } if _, ok := allMethod[signature.HTTPMethod]; !ok { return fmt.Errorf("invalid method: %s", signature.HTTPMethod) } o.RouterProperties = append(o.RouterProperties, signature) return nil } func (o *OperationV3) ParseServerURLComment(commentLine string) error { server := spec.NewServer() server.Spec.URL = commentLine o.Servers = append(o.Servers, server) return nil } func (o *OperationV3) ParseServerDescriptionComment(commentLine string) error { lastAddedServer := o.Servers[len(o.Servers)-1] lastAddedServer.Spec.Description = commentLine return nil } // createParameter returns swagger spec.Parameter for given paramType, description, paramName, schemaType, required. func createParameterV3(in, description, paramName, objectType, schemaType string, required bool, enums []interface{}, collectionFormat string) spec.Parameter { // //five possible parameter types. query, path, body, header, form result := spec.Parameter{ Description: description, Required: required, Name: paramName, In: in, Schema: spec.NewRefOrSpec(nil, &spec.Schema{}), } if in == "body" { return result } switch objectType { case ARRAY: result.Schema.Spec.Type = spec.NewSingleOrArray(objectType) result.Schema.Spec.Items = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) result.Schema.Spec.Items.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) result.Schema.Spec.Enum = enums case PRIMITIVE, OBJECT: result.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) result.Schema.Spec.Enum = enums } return result } func (o *OperationV3) parseObjectSchema(refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { return parseObjectSchemaV3(o.parser, refType, astFile) } func parseObjectSchemaV3(parser *Parser, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { switch { case refType == NIL: return nil, nil case refType == INTERFACE: return PrimitiveSchemaV3(OBJECT), nil case refType == ANY: return PrimitiveSchemaV3(OBJECT), nil case IsGolangPrimitiveType(refType): refType = TransToValidSchemeType(refType) return PrimitiveSchemaV3(refType), nil case IsPrimitiveType(refType): return PrimitiveSchemaV3(refType), nil case strings.HasPrefix(refType, "[]"): schema, err := parseObjectSchemaV3(parser, refType[2:], astFile) if err != nil { return nil, err } result := spec.NewSchemaSpec() result.Spec.Type = spec.NewSingleOrArray("array") result.Spec.Items = spec.NewBoolOrSchema(false, schema) return result, nil case strings.HasPrefix(refType, "map["): // ignore key type idx := strings.Index(refType, "]") if idx < 0 { return nil, fmt.Errorf("invalid type: %s", refType) } refType = refType[idx+1:] if refType == INTERFACE || refType == ANY { schema := &spec.Schema{} schema.AdditionalProperties = spec.NewBoolOrSchema(false, spec.NewSchemaSpec()) schema.Type = spec.NewSingleOrArray(OBJECT) refOrSpec := spec.NewRefOrSpec(nil, schema) return refOrSpec, nil } schema, err := parseObjectSchemaV3(parser, refType, astFile) if err != nil { return nil, err } result := &spec.Schema{} result.AdditionalProperties = spec.NewBoolOrSchema(false, schema) result.Type = spec.NewSingleOrArray(OBJECT) refOrSpec := spec.NewSchemaSpec() refOrSpec.Spec = result return refOrSpec, nil case strings.Contains(refType, "{"): return parseCombinedObjectSchemaV3(parser, refType, astFile) default: if parser != nil { // checking refType has existing in 'TypeDefinitions' schema, err := parser.getTypeSchemaV3(refType, astFile, true) if err != nil { return nil, err } return schema, nil } return spec.NewSchemaRef(spec.NewRef("#/components/schemas/" + refType)), nil } } // ParseResponseHeaderComment parses comment for given `response header` comment string. func (o *OperationV3) ParseResponseHeaderComment(commentLine string, _ *ast.File) error { matches := responsePattern.FindStringSubmatch(commentLine) if len(matches) != 5 { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } header := newHeaderSpecV3(strings.Trim(matches[2], "{}"), strings.Trim(matches[4], "\"")) headerKey := strings.TrimSpace(matches[3]) if strings.EqualFold(matches[1], "all") { if o.Responses.Spec.Default != nil { o.Responses.Spec.Default.Spec.Spec.Headers[headerKey] = header } if o.Responses.Spec.Response != nil { for _, v := range o.Responses.Spec.Response { v.Spec.Spec.Headers[headerKey] = header } } return nil } for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { if o.Responses.Spec.Default != nil { o.Responses.Spec.Default.Spec.Spec.Headers[headerKey] = header } continue } _, err := strconv.Atoi(codeStr) if err != nil { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } // TODO check condition if o.Responses != nil && o.Responses.Spec != nil && o.Responses.Spec.Response != nil { response, responseExist := o.Responses.Spec.Response[codeStr] if responseExist { response.Spec.Spec.Headers[headerKey] = header o.Responses.Spec.Response[codeStr] = response } } } return nil } func newHeaderSpecV3(schemaType, description string) *spec.RefOrSpec[spec.Extendable[spec.Header]] { result := spec.NewHeaderSpec() result.Spec.Spec.Description = description result.Spec.Spec.Schema = spec.NewSchemaSpec() result.Spec.Spec.Schema.Spec.Type = spec.NewSingleOrArray(schemaType) return result } // ParseResponseComment parses comment for given `response` comment string. func (o *OperationV3) ParseResponseComment(commentLine string, astFile *ast.File) error { matches := responsePattern.FindStringSubmatch(commentLine) if len(matches) != 5 { err := o.ParseEmptyResponseComment(commentLine) if err != nil { return o.ParseEmptyResponseOnly(commentLine) } return err } description := strings.Trim(matches[4], "\"") schema, err := o.parseAPIObjectSchema(commentLine, strings.Trim(matches[2], "{}"), strings.TrimSpace(matches[3]), astFile) if err != nil { return err } for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { codeStr = "" } else { code, err := strconv.Atoi(codeStr) if err != nil { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } if description == "" { description = http.StatusText(code) } } response := spec.NewResponseSpec() response.Spec.Spec.Description = description mimeType := "application/json" // TODO: set correct mimeType setResponseSchema(response.Spec.Spec, mimeType, schema) o.AddResponse(codeStr, response) } return nil } // setResponseSchema sets response schema for given response. func setResponseSchema(response *spec.Response, mimeType string, schema *spec.RefOrSpec[spec.Schema]) { mediaType := spec.NewMediaType() mediaType.Spec.Schema = schema if response.Content == nil { response.Content = make(map[string]*spec.Extendable[spec.MediaType]) } response.Content[mimeType] = mediaType } // ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok". func (o *OperationV3) ParseEmptyResponseComment(commentLine string) error { matches := emptyResponsePattern.FindStringSubmatch(commentLine) if len(matches) != 3 { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } description := strings.Trim(matches[2], "\"") for _, codeStr := range strings.Split(matches[1], ",") { if strings.EqualFold(codeStr, defaultTag) { codeStr = "" } else { _, err := strconv.Atoi(codeStr) if err != nil { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } } o.AddResponse(codeStr, newResponseWithDescription(description)) } return nil } // AddResponse add a response for a code. // If the code is already exist, it will merge with the old one: // 1. The description will be replaced by the new one if the new one is not empty. // 2. The content schema will be merged using `oneOf` if the new one is not empty. func (o *OperationV3) AddResponse(code string, response *spec.RefOrSpec[spec.Extendable[spec.Response]]) { if response.Spec.Spec.Headers == nil { response.Spec.Spec.Headers = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Header]]) } if o.Responses.Spec.Response == nil { o.Responses.Spec.Response = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Response]]) } res := response var prev *spec.RefOrSpec[spec.Extendable[spec.Response]] if code != "" { prev = o.Responses.Spec.Response[code] } else { prev = o.Responses.Spec.Default } if prev != nil { // merge into prev res = prev if response.Spec.Spec.Description != "" { prev.Spec.Spec.Description = response.Spec.Spec.Description } if len(response.Spec.Spec.Content) > 0 { // responses should only have one content type singleKey := "" for k := range response.Spec.Spec.Content { singleKey = k break } if prevMediaType := prev.Spec.Spec.Content[singleKey]; prevMediaType == nil { prev.Spec.Spec.Content = response.Spec.Spec.Content } else { newMediaType := response.Spec.Spec.Content[singleKey] if len(newMediaType.Extensions) > 0 { if prevMediaType.Extensions == nil { prevMediaType.Extensions = make(map[string]interface{}) } for k, v := range newMediaType.Extensions { prevMediaType.Extensions[k] = v } } if len(newMediaType.Spec.Examples) > 0 { if prevMediaType.Spec.Examples == nil { prevMediaType.Spec.Examples = make(map[string]*spec.RefOrSpec[spec.Extendable[spec.Example]]) } for k, v := range newMediaType.Spec.Examples { prevMediaType.Spec.Examples[k] = v } } if prevSchema := prevMediaType.Spec.Schema; prevSchema.Ref != nil || prevSchema.Spec.OneOf == nil { oneOfSchema := spec.NewSchemaSpec() oneOfSchema.Spec.OneOf = []*spec.RefOrSpec[spec.Schema]{prevSchema, newMediaType.Spec.Schema} prevMediaType.Spec.Schema = oneOfSchema } else { prevSchema.Spec.OneOf = append(prevSchema.Spec.OneOf, newMediaType.Spec.Schema) } } } } if code != "" { o.Responses.Spec.Response[code] = res } else { o.Responses.Spec.Default = res } } // ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200. func (o *OperationV3) ParseEmptyResponseOnly(commentLine string) error { for _, codeStr := range strings.Split(commentLine, ",") { var description string if strings.EqualFold(codeStr, defaultTag) { codeStr = "" } else { code, err := strconv.Atoi(codeStr) if err != nil { return fmt.Errorf("can not parse response comment \"%s\"", commentLine) } description = http.StatusText(code) } o.AddResponse(codeStr, newResponseWithDescription(description)) } return nil } func newResponseWithDescription(description string) *spec.RefOrSpec[spec.Extendable[spec.Response]] { response := spec.NewResponseSpec() response.Spec.Spec.Description = description return response } func parseCombinedObjectSchemaV3(parser *Parser, refType string, astFile *ast.File) (*spec.RefOrSpec[spec.Schema], error) { matches := combinedPattern.FindStringSubmatch(refType) if len(matches) != 3 { return nil, fmt.Errorf("invalid type: %s", refType) } schema, err := parseObjectSchemaV3(parser, matches[1], astFile) if err != nil { return nil, err } fields, props := parseFields(matches[2]), map[string]*spec.RefOrSpec[spec.Schema]{} for _, field := range fields { keyVal := strings.SplitN(field, "=", 2) if len(keyVal) != 2 { continue } schema, err := parseObjectSchemaV3(parser, keyVal[1], astFile) if err != nil { return nil, err } props[keyVal[0]] = schema } if len(props) == 0 { return schema, nil } if schema.Ref == nil && len(schema.Spec.Type) > 0 && schema.Spec.Type[0] == OBJECT && len(schema.Spec.Properties) == 0 && schema.Spec.AdditionalProperties == nil { schema.Spec.Properties = props return schema, nil } schemaRefPath := strings.Replace(schema.Ref.Ref, "#/components/schemas/", "", 1) schemaSpec := parser.openAPI.Components.Spec.Schemas[schemaRefPath] schemaSpec.Spec.JsonSchemaComposition.AllOf = make([]*spec.RefOrSpec[spec.Schema], len(props)) i := 0 for name, prop := range props { wrapperSpec := spec.NewSchemaSpec() wrapperSpec.Spec = &spec.Schema{} wrapperSpec.Spec.Type = spec.NewSingleOrArray(OBJECT) wrapperSpec.Spec.Properties = map[string]*spec.RefOrSpec[spec.Schema]{ name: prop, } parser.openAPI.Components.Spec.Schemas[name] = wrapperSpec ref := spec.NewRefOrSpec[spec.Schema](spec.NewRef("#/components/schemas/"+name), nil) schemaSpec.Spec.JsonSchemaComposition.AllOf[i] = ref i++ } return schemaSpec, nil } // ParseSecurityComment parses comment for given `security` comment string. func (o *OperationV3) ParseSecurityComment(commentLine string) error { var ( securityMap = make(map[string][]string) securitySource = commentLine[strings.Index(commentLine, "@Security")+1:] ) for _, securityOption := range strings.Split(securitySource, "||") { securityOption = strings.TrimSpace(securityOption) left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]") if !(left == -1 && right == -1) { scopes := securityOption[left+1 : right] var options []string for _, scope := range strings.Split(scopes, ",") { options = append(options, strings.TrimSpace(scope)) } securityKey := securityOption[0:left] securityMap[securityKey] = append(securityMap[securityKey], options...) } else { securityKey := strings.TrimSpace(securityOption) securityMap[securityKey] = []string{} } } o.Security = append(o.Security, securityMap) return nil } // ParseCodeSample godoc. func (o *OperationV3) ParseCodeSample(attribute, _, lineRemainder string) error { log.Println("line remainder:", lineRemainder) if lineRemainder == "file" { log.Println("line remainder is file") data, isJSON, err := getCodeExampleForSummary(o.Summary, o.codeExampleFilesDir) if err != nil { return err } // using custom type, as json marshaller has problems with []map[interface{}]map[interface{}]interface{} var valueJSON CodeSamples if isJSON { err = json.Unmarshal(data, &valueJSON) if err != nil { return fmt.Errorf("annotation %s need a valid json value. error: %s", attribute, err.Error()) } } else { err = yaml.Unmarshal(data, &valueJSON) if err != nil { return fmt.Errorf("annotation %s need a valid yaml value. error: %s", attribute, err.Error()) } } o.Responses.Extensions[attribute[1:]] = valueJSON return nil } // Fallback into existing logic return o.ParseMetadata(attribute, strings.ToLower(attribute), lineRemainder) }