forked from ebhomengo/niki
1235 lines
35 KiB
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, ¶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)
|
|
}
|