niki/vendor/github.com/swaggo/swag/v2/operationv3.go

1235 lines
35 KiB
Go

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, &param)
if err != nil {
return err
}
item := spec.NewRefOrSpec(nil, &spec.Extendable[spec.Parameter]{
Spec: &param,
})
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)
}