operation.go 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230
  1. package swag
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "go/ast"
  6. goparser "go/parser"
  7. "go/token"
  8. "net/http"
  9. "os"
  10. "path/filepath"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "github.com/go-openapi/spec"
  15. "golang.org/x/tools/go/loader"
  16. )
  17. // RouteProperties describes HTTP properties of a single router comment.
  18. type RouteProperties struct {
  19. HTTPMethod string
  20. Path string
  21. }
  22. // Operation describes a single API operation on a path.
  23. // For more information: https://github.com/swaggo/swag#api-operation
  24. type Operation struct {
  25. parser *Parser
  26. codeExampleFilesDir string
  27. spec.Operation
  28. RouterProperties []RouteProperties
  29. }
  30. var mimeTypeAliases = map[string]string{
  31. "json": "application/json",
  32. "xml": "text/xml",
  33. "plain": "text/plain",
  34. "html": "text/html",
  35. "mpfd": "multipart/form-data",
  36. "x-www-form-urlencoded": "application/x-www-form-urlencoded",
  37. "json-api": "application/vnd.api+json",
  38. "json-stream": "application/x-json-stream",
  39. "octet-stream": "application/octet-stream",
  40. "png": "image/png",
  41. "jpeg": "image/jpeg",
  42. "gif": "image/gif",
  43. }
  44. var mimeTypePattern = regexp.MustCompile("^[^/]+/[^/]+$")
  45. // NewOperation creates a new Operation with default properties.
  46. // map[int]Response.
  47. func NewOperation(parser *Parser, options ...func(*Operation)) *Operation {
  48. if parser == nil {
  49. parser = New()
  50. }
  51. result := &Operation{
  52. parser: parser,
  53. RouterProperties: []RouteProperties{},
  54. Operation: spec.Operation{
  55. OperationProps: spec.OperationProps{
  56. ID: "",
  57. Description: "",
  58. Summary: "",
  59. Security: nil,
  60. ExternalDocs: nil,
  61. Deprecated: false,
  62. Tags: []string{},
  63. Consumes: []string{},
  64. Produces: []string{},
  65. Schemes: []string{},
  66. Parameters: []spec.Parameter{},
  67. Responses: &spec.Responses{
  68. VendorExtensible: spec.VendorExtensible{
  69. Extensions: spec.Extensions{},
  70. },
  71. ResponsesProps: spec.ResponsesProps{
  72. Default: nil,
  73. StatusCodeResponses: make(map[int]spec.Response),
  74. },
  75. },
  76. },
  77. VendorExtensible: spec.VendorExtensible{
  78. Extensions: spec.Extensions{},
  79. },
  80. },
  81. codeExampleFilesDir: "",
  82. }
  83. for _, option := range options {
  84. option(result)
  85. }
  86. return result
  87. }
  88. // SetCodeExampleFilesDirectory sets the directory to search for codeExamples.
  89. func SetCodeExampleFilesDirectory(directoryPath string) func(*Operation) {
  90. return func(o *Operation) {
  91. o.codeExampleFilesDir = directoryPath
  92. }
  93. }
  94. // ParseComment parses comment for given comment string and returns error if error occurs.
  95. func (operation *Operation) ParseComment(comment string, astFile *ast.File) error {
  96. commentLine := strings.TrimSpace(strings.TrimLeft(comment, "/"))
  97. if len(commentLine) == 0 {
  98. return nil
  99. }
  100. attribute := strings.Fields(commentLine)[0]
  101. lineRemainder, lowerAttribute := strings.TrimSpace(commentLine[len(attribute):]), strings.ToLower(attribute)
  102. switch lowerAttribute {
  103. case descriptionAttr:
  104. operation.ParseDescriptionComment(lineRemainder)
  105. case descriptionMarkdownAttr:
  106. commentInfo, err := getMarkdownForTag(lineRemainder, operation.parser.markdownFileDir)
  107. if err != nil {
  108. return err
  109. }
  110. operation.ParseDescriptionComment(string(commentInfo))
  111. case summaryAttr:
  112. operation.Summary = lineRemainder
  113. case idAttr:
  114. operation.ID = lineRemainder
  115. case tagsAttr:
  116. operation.ParseTagsComment(lineRemainder)
  117. case acceptAttr:
  118. return operation.ParseAcceptComment(lineRemainder)
  119. case produceAttr:
  120. return operation.ParseProduceComment(lineRemainder)
  121. case paramAttr:
  122. return operation.ParseParamComment(lineRemainder, astFile)
  123. case successAttr, failureAttr, responseAttr:
  124. return operation.ParseResponseComment(lineRemainder, astFile)
  125. case headerAttr:
  126. return operation.ParseResponseHeaderComment(lineRemainder, astFile)
  127. case routerAttr:
  128. return operation.ParseRouterComment(lineRemainder)
  129. case securityAttr:
  130. return operation.ParseSecurityComment(lineRemainder)
  131. case deprecatedAttr:
  132. operation.Deprecate()
  133. case xCodeSamplesAttr:
  134. return operation.ParseCodeSample(attribute, commentLine, lineRemainder)
  135. default:
  136. return operation.ParseMetadata(attribute, lowerAttribute, lineRemainder)
  137. }
  138. return nil
  139. }
  140. // ParseCodeSample godoc.
  141. func (operation *Operation) ParseCodeSample(attribute, _, lineRemainder string) error {
  142. if lineRemainder == "file" {
  143. data, err := getCodeExampleForSummary(operation.Summary, operation.codeExampleFilesDir)
  144. if err != nil {
  145. return err
  146. }
  147. var valueJSON interface{}
  148. err = json.Unmarshal(data, &valueJSON)
  149. if err != nil {
  150. return fmt.Errorf("annotation %s need a valid json value", attribute)
  151. }
  152. // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly
  153. operation.Extensions[attribute[1:]] = valueJSON
  154. return nil
  155. }
  156. // Fallback into existing logic
  157. return operation.ParseMetadata(attribute, strings.ToLower(attribute), lineRemainder)
  158. }
  159. // ParseDescriptionComment godoc.
  160. func (operation *Operation) ParseDescriptionComment(lineRemainder string) {
  161. if operation.Description == "" {
  162. operation.Description = lineRemainder
  163. return
  164. }
  165. operation.Description += "\n" + lineRemainder
  166. }
  167. // ParseMetadata godoc.
  168. func (operation *Operation) ParseMetadata(attribute, lowerAttribute, lineRemainder string) error {
  169. // parsing specific meta data extensions
  170. if strings.HasPrefix(lowerAttribute, "@x-") {
  171. if len(lineRemainder) == 0 {
  172. return fmt.Errorf("annotation %s need a value", attribute)
  173. }
  174. var valueJSON interface{}
  175. err := json.Unmarshal([]byte(lineRemainder), &valueJSON)
  176. if err != nil {
  177. return fmt.Errorf("annotation %s need a valid json value", attribute)
  178. }
  179. // don't use the method provided by spec lib, because it will call toLower() on attribute names, which is wrongly
  180. operation.Extensions[attribute[1:]] = valueJSON
  181. }
  182. return nil
  183. }
  184. var paramPattern = regexp.MustCompile(`(\S+)\s+(\w+)\s+([\S. ]+?)\s+(\w+)\s+"([^"]+)"`)
  185. func findInSlice(arr []string, target string) bool {
  186. for _, str := range arr {
  187. if str == target {
  188. return true
  189. }
  190. }
  191. return false
  192. }
  193. func (operation *Operation) parseArrayParam(param *spec.Parameter, paramType, refType, objectType string) error {
  194. if !IsPrimitiveType(refType) && !(refType == "file" && paramType == "formData") {
  195. return fmt.Errorf("%s is not supported array type for %s", refType, paramType)
  196. }
  197. param.SimpleSchema.Type = objectType
  198. if operation.parser != nil {
  199. param.CollectionFormat = TransToValidCollectionFormat(operation.parser.collectionFormatInQuery)
  200. }
  201. param.SimpleSchema.Items = &spec.Items{
  202. SimpleSchema: spec.SimpleSchema{
  203. Default: nil,
  204. Nullable: false,
  205. Format: "",
  206. Items: nil,
  207. CollectionFormat: "",
  208. Type: refType,
  209. Example: nil,
  210. },
  211. CommonValidations: spec.CommonValidations{
  212. Maximum: nil,
  213. ExclusiveMaximum: false,
  214. Minimum: nil,
  215. ExclusiveMinimum: false,
  216. MaxLength: nil,
  217. MinLength: nil,
  218. Pattern: "",
  219. MaxItems: nil,
  220. MinItems: nil,
  221. UniqueItems: false,
  222. MultipleOf: nil,
  223. Enum: nil,
  224. },
  225. VendorExtensible: spec.VendorExtensible{
  226. Extensions: nil,
  227. },
  228. }
  229. return nil
  230. }
  231. // ParseParamComment parses params return []string of param properties
  232. // E.g. @Param queryText formData string true "The email for login"
  233. //
  234. // [param name] [paramType] [data type] [is mandatory?] [Comment]
  235. //
  236. // E.g. @Param some_id path int true "Some ID".
  237. func (operation *Operation) ParseParamComment(commentLine string, astFile *ast.File) error {
  238. matches := paramPattern.FindStringSubmatch(commentLine)
  239. if len(matches) != 6 {
  240. return fmt.Errorf("missing required param comment parameters \"%s\"", commentLine)
  241. }
  242. name := matches[1]
  243. paramType := matches[2]
  244. refType := TransToValidSchemeType(matches[3])
  245. // Detect refType
  246. objectType := OBJECT
  247. if strings.HasPrefix(refType, "[]") {
  248. objectType = ARRAY
  249. refType = strings.TrimPrefix(refType, "[]")
  250. refType = TransToValidSchemeType(refType)
  251. } else if IsPrimitiveType(refType) ||
  252. paramType == "formData" && refType == "file" {
  253. objectType = PRIMITIVE
  254. }
  255. requiredText := strings.ToLower(matches[4])
  256. required := requiredText == "true" || requiredText == requiredLabel
  257. description := matches[5]
  258. param := createParameter(paramType, description, name, refType, required)
  259. switch paramType {
  260. case "path", "header":
  261. switch objectType {
  262. case ARRAY:
  263. err := operation.parseArrayParam(&param, paramType, refType, objectType)
  264. if err != nil {
  265. return err
  266. }
  267. case OBJECT:
  268. return fmt.Errorf("%s is not supported type for %s", refType, paramType)
  269. }
  270. case "query", "formData":
  271. switch objectType {
  272. case ARRAY:
  273. err := operation.parseArrayParam(&param, paramType, refType, objectType)
  274. if err != nil {
  275. return err
  276. }
  277. case OBJECT:
  278. schema, err := operation.parser.getTypeSchema(refType, astFile, false)
  279. if err != nil {
  280. return err
  281. }
  282. if len(schema.Properties) == 0 {
  283. return nil
  284. }
  285. items := schema.Properties.ToOrderedSchemaItems()
  286. for _, item := range items {
  287. name, prop := item.Name, item.Schema
  288. if len(prop.Type) == 0 {
  289. continue
  290. }
  291. switch {
  292. case prop.Type[0] == ARRAY && prop.Items.Schema != nil &&
  293. len(prop.Items.Schema.Type) > 0 && IsSimplePrimitiveType(prop.Items.Schema.Type[0]):
  294. param = createParameter(paramType, prop.Description, name, prop.Type[0], findInSlice(schema.Required, name))
  295. param.SimpleSchema.Type = prop.Type[0]
  296. if operation.parser != nil && operation.parser.collectionFormatInQuery != "" && param.CollectionFormat == "" {
  297. param.CollectionFormat = TransToValidCollectionFormat(operation.parser.collectionFormatInQuery)
  298. }
  299. param.SimpleSchema.Items = &spec.Items{
  300. SimpleSchema: spec.SimpleSchema{
  301. Type: prop.Items.Schema.Type[0],
  302. },
  303. }
  304. case IsSimplePrimitiveType(prop.Type[0]):
  305. param = createParameter(paramType, prop.Description, name, prop.Type[0], findInSlice(schema.Required, name))
  306. default:
  307. operation.parser.debug.Printf("skip field [%s] in %s is not supported type for %s", name, refType, paramType)
  308. continue
  309. }
  310. param.Nullable = prop.Nullable
  311. param.Format = prop.Format
  312. param.Default = prop.Default
  313. param.Example = prop.Example
  314. param.Extensions = prop.Extensions
  315. param.CommonValidations.Maximum = prop.Maximum
  316. param.CommonValidations.Minimum = prop.Minimum
  317. param.CommonValidations.ExclusiveMaximum = prop.ExclusiveMaximum
  318. param.CommonValidations.ExclusiveMinimum = prop.ExclusiveMinimum
  319. param.CommonValidations.MaxLength = prop.MaxLength
  320. param.CommonValidations.MinLength = prop.MinLength
  321. param.CommonValidations.Pattern = prop.Pattern
  322. param.CommonValidations.MaxItems = prop.MaxItems
  323. param.CommonValidations.MinItems = prop.MinItems
  324. param.CommonValidations.UniqueItems = prop.UniqueItems
  325. param.CommonValidations.MultipleOf = prop.MultipleOf
  326. param.CommonValidations.Enum = prop.Enum
  327. operation.Operation.Parameters = append(operation.Operation.Parameters, param)
  328. }
  329. return nil
  330. }
  331. case "body":
  332. if objectType == PRIMITIVE {
  333. param.Schema = PrimitiveSchema(refType)
  334. } else {
  335. schema, err := operation.parseAPIObjectSchema(commentLine, objectType, refType, astFile)
  336. if err != nil {
  337. return err
  338. }
  339. param.Schema = schema
  340. }
  341. default:
  342. return fmt.Errorf("%s is not supported paramType", paramType)
  343. }
  344. err := operation.parseParamAttribute(commentLine, objectType, refType, &param)
  345. if err != nil {
  346. return err
  347. }
  348. operation.Operation.Parameters = append(operation.Operation.Parameters, param)
  349. return nil
  350. }
  351. const (
  352. jsonTag = "json"
  353. bindingTag = "binding"
  354. defaultTag = "default"
  355. enumsTag = "enums"
  356. exampleTag = "example"
  357. schemaExampleTag = "schemaExample"
  358. formatTag = "format"
  359. validateTag = "validate"
  360. minimumTag = "minimum"
  361. maximumTag = "maximum"
  362. minLengthTag = "minLength"
  363. maxLengthTag = "maxLength"
  364. multipleOfTag = "multipleOf"
  365. readOnlyTag = "readonly"
  366. extensionsTag = "extensions"
  367. collectionFormatTag = "collectionFormat"
  368. )
  369. var regexAttributes = map[string]*regexp.Regexp{
  370. // for Enums(A, B)
  371. enumsTag: regexp.MustCompile(`(?i)\s+enums\(.*\)`),
  372. // for maximum(0)
  373. maximumTag: regexp.MustCompile(`(?i)\s+maxinum|maximum\(.*\)`),
  374. // for minimum(0)
  375. minimumTag: regexp.MustCompile(`(?i)\s+mininum|minimum\(.*\)`),
  376. // for default(0)
  377. defaultTag: regexp.MustCompile(`(?i)\s+default\(.*\)`),
  378. // for minlength(0)
  379. minLengthTag: regexp.MustCompile(`(?i)\s+minlength\(.*\)`),
  380. // for maxlength(0)
  381. maxLengthTag: regexp.MustCompile(`(?i)\s+maxlength\(.*\)`),
  382. // for format(email)
  383. formatTag: regexp.MustCompile(`(?i)\s+format\(.*\)`),
  384. // for extensions(x-example=test)
  385. extensionsTag: regexp.MustCompile(`(?i)\s+extensions\(.*\)`),
  386. // for collectionFormat(csv)
  387. collectionFormatTag: regexp.MustCompile(`(?i)\s+collectionFormat\(.*\)`),
  388. // example(0)
  389. exampleTag: regexp.MustCompile(`(?i)\s+example\(.*\)`),
  390. // schemaExample(0)
  391. schemaExampleTag: regexp.MustCompile(`(?i)\s+schemaExample\(.*\)`),
  392. }
  393. func (operation *Operation) parseParamAttribute(comment, objectType, schemaType string, param *spec.Parameter) error {
  394. schemaType = TransToValidSchemeType(schemaType)
  395. for attrKey, re := range regexAttributes {
  396. attr, err := findAttr(re, comment)
  397. if err != nil {
  398. continue
  399. }
  400. switch attrKey {
  401. case enumsTag:
  402. err = setEnumParam(param, attr, objectType, schemaType)
  403. case minimumTag, maximumTag:
  404. err = setNumberParam(param, attrKey, schemaType, attr, comment)
  405. case defaultTag:
  406. err = setDefault(param, schemaType, attr)
  407. case minLengthTag, maxLengthTag:
  408. err = setStringParam(param, attrKey, schemaType, attr, comment)
  409. case formatTag:
  410. param.Format = attr
  411. case exampleTag:
  412. err = setExample(param, schemaType, attr)
  413. case schemaExampleTag:
  414. err = setSchemaExample(param, schemaType, attr)
  415. case extensionsTag:
  416. param.Extensions = setExtensionParam(attr)
  417. case collectionFormatTag:
  418. err = setCollectionFormatParam(param, attrKey, objectType, attr, comment)
  419. }
  420. if err != nil {
  421. return err
  422. }
  423. }
  424. return nil
  425. }
  426. func findAttr(re *regexp.Regexp, commentLine string) (string, error) {
  427. attr := re.FindString(commentLine)
  428. l, r := strings.Index(attr, "("), strings.Index(attr, ")")
  429. if l == -1 || r == -1 {
  430. return "", fmt.Errorf("can not find regex=%s, comment=%s", re.String(), commentLine)
  431. }
  432. return strings.TrimSpace(attr[l+1 : r]), nil
  433. }
  434. func setStringParam(param *spec.Parameter, name, schemaType, attr, commentLine string) error {
  435. if schemaType != STRING {
  436. return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType)
  437. }
  438. n, err := strconv.ParseInt(attr, 10, 64)
  439. if err != nil {
  440. return fmt.Errorf("%s is allow only a number got=%s", name, attr)
  441. }
  442. switch name {
  443. case minLengthTag:
  444. param.MinLength = &n
  445. case maxLengthTag:
  446. param.MaxLength = &n
  447. }
  448. return nil
  449. }
  450. func setNumberParam(param *spec.Parameter, name, schemaType, attr, commentLine string) error {
  451. switch schemaType {
  452. case INTEGER, NUMBER:
  453. n, err := strconv.ParseFloat(attr, 64)
  454. if err != nil {
  455. return fmt.Errorf("maximum is allow only a number. comment=%s got=%s", commentLine, attr)
  456. }
  457. switch name {
  458. case minimumTag:
  459. param.Minimum = &n
  460. case maximumTag:
  461. param.Maximum = &n
  462. }
  463. return nil
  464. default:
  465. return fmt.Errorf("%s is attribute to set to a number. comment=%s got=%s", name, commentLine, schemaType)
  466. }
  467. }
  468. func setEnumParam(param *spec.Parameter, attr, objectType, schemaType string) error {
  469. for _, e := range strings.Split(attr, ",") {
  470. e = strings.TrimSpace(e)
  471. value, err := defineType(schemaType, e)
  472. if err != nil {
  473. return err
  474. }
  475. switch objectType {
  476. case ARRAY:
  477. param.Items.Enum = append(param.Items.Enum, value)
  478. default:
  479. param.Enum = append(param.Enum, value)
  480. }
  481. }
  482. return nil
  483. }
  484. func setExtensionParam(attr string) spec.Extensions {
  485. extensions := spec.Extensions{}
  486. for _, val := range splitNotWrapped(attr, ',') {
  487. parts := strings.SplitN(val, "=", 2)
  488. if len(parts) == 2 {
  489. extensions.Add(parts[0], parts[1])
  490. continue
  491. }
  492. if len(parts[0]) > 0 && string(parts[0][0]) == "!" {
  493. extensions.Add(parts[0][1:], false)
  494. continue
  495. }
  496. extensions.Add(parts[0], true)
  497. }
  498. return extensions
  499. }
  500. func setCollectionFormatParam(param *spec.Parameter, name, schemaType, attr, commentLine string) error {
  501. if schemaType == ARRAY {
  502. param.CollectionFormat = TransToValidCollectionFormat(attr)
  503. return nil
  504. }
  505. return fmt.Errorf("%s is attribute to set to an array. comment=%s got=%s", name, commentLine, schemaType)
  506. }
  507. func setDefault(param *spec.Parameter, schemaType string, value string) error {
  508. val, err := defineType(schemaType, value)
  509. if err != nil {
  510. return nil // Don't set a default value if it's not valid
  511. }
  512. param.Default = val
  513. return nil
  514. }
  515. func setSchemaExample(param *spec.Parameter, schemaType string, value string) error {
  516. val, err := defineType(schemaType, value)
  517. if err != nil {
  518. return nil // Don't set a example value if it's not valid
  519. }
  520. // skip schema
  521. if param.Schema == nil {
  522. return nil
  523. }
  524. switch v := val.(type) {
  525. case string:
  526. // replaces \r \n \t in example string values.
  527. param.Schema.Example = strings.NewReplacer(`\r`, "\r", `\n`, "\n", `\t`, "\t").Replace(v)
  528. default:
  529. param.Schema.Example = val
  530. }
  531. return nil
  532. }
  533. func setExample(param *spec.Parameter, schemaType string, value string) error {
  534. val, err := defineType(schemaType, value)
  535. if err != nil {
  536. return nil // Don't set a example value if it's not valid
  537. }
  538. param.Example = val
  539. return nil
  540. }
  541. // defineType enum value define the type (object and array unsupported).
  542. func defineType(schemaType string, value string) (v interface{}, err error) {
  543. schemaType = TransToValidSchemeType(schemaType)
  544. switch schemaType {
  545. case STRING:
  546. return value, nil
  547. case NUMBER:
  548. v, err = strconv.ParseFloat(value, 64)
  549. if err != nil {
  550. return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err)
  551. }
  552. case INTEGER:
  553. v, err = strconv.Atoi(value)
  554. if err != nil {
  555. return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err)
  556. }
  557. case BOOLEAN:
  558. v, err = strconv.ParseBool(value)
  559. if err != nil {
  560. return nil, fmt.Errorf("enum value %s can't convert to %s err: %s", value, schemaType, err)
  561. }
  562. default:
  563. return nil, fmt.Errorf("%s is unsupported type in enum value %s", schemaType, value)
  564. }
  565. return v, nil
  566. }
  567. // ParseTagsComment parses comment for given `tag` comment string.
  568. func (operation *Operation) ParseTagsComment(commentLine string) {
  569. for _, tag := range strings.Split(commentLine, ",") {
  570. operation.Tags = append(operation.Tags, strings.TrimSpace(tag))
  571. }
  572. }
  573. // ParseAcceptComment parses comment for given `accept` comment string.
  574. func (operation *Operation) ParseAcceptComment(commentLine string) error {
  575. return parseMimeTypeList(commentLine, &operation.Consumes, "%v accept type can't be accepted")
  576. }
  577. // ParseProduceComment parses comment for given `produce` comment string.
  578. func (operation *Operation) ParseProduceComment(commentLine string) error {
  579. return parseMimeTypeList(commentLine, &operation.Produces, "%v produce type can't be accepted")
  580. }
  581. // parseMimeTypeList parses a list of MIME Types for a comment like
  582. // `produce` (`Content-Type:` response header) or
  583. // `accept` (`Accept:` request header).
  584. func parseMimeTypeList(mimeTypeList string, typeList *[]string, format string) error {
  585. for _, typeName := range strings.Split(mimeTypeList, ",") {
  586. if mimeTypePattern.MatchString(typeName) {
  587. *typeList = append(*typeList, typeName)
  588. continue
  589. }
  590. aliasMimeType, ok := mimeTypeAliases[typeName]
  591. if !ok {
  592. return fmt.Errorf(format, typeName)
  593. }
  594. *typeList = append(*typeList, aliasMimeType)
  595. }
  596. return nil
  597. }
  598. var routerPattern = regexp.MustCompile(`^(/[\w./\-{}+:$]*)[[:blank:]]+\[(\w+)]`)
  599. // ParseRouterComment parses comment for given `router` comment string.
  600. func (operation *Operation) ParseRouterComment(commentLine string) error {
  601. matches := routerPattern.FindStringSubmatch(commentLine)
  602. if len(matches) != 3 {
  603. return fmt.Errorf("can not parse router comment \"%s\"", commentLine)
  604. }
  605. signature := RouteProperties{
  606. Path: matches[1],
  607. HTTPMethod: strings.ToUpper(matches[2]),
  608. }
  609. if _, ok := allMethod[signature.HTTPMethod]; !ok {
  610. return fmt.Errorf("invalid method: %s", signature.HTTPMethod)
  611. }
  612. operation.RouterProperties = append(operation.RouterProperties, signature)
  613. return nil
  614. }
  615. // ParseSecurityComment parses comment for given `security` comment string.
  616. func (operation *Operation) ParseSecurityComment(commentLine string) error {
  617. var (
  618. securityMap = make(map[string][]string)
  619. securitySource = commentLine[strings.Index(commentLine, "@Security")+1:]
  620. )
  621. for _, securityOption := range strings.Split(securitySource, "||") {
  622. securityOption = strings.TrimSpace(securityOption)
  623. left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]")
  624. if !(left == -1 && right == -1) {
  625. scopes := securityOption[left+1 : right]
  626. var options []string
  627. for _, scope := range strings.Split(scopes, ",") {
  628. options = append(options, strings.TrimSpace(scope))
  629. }
  630. securityKey := securityOption[0:left]
  631. securityMap[securityKey] = append(securityMap[securityKey], options...)
  632. } else {
  633. securityKey := strings.TrimSpace(securityOption)
  634. securityMap[securityKey] = []string{}
  635. }
  636. }
  637. operation.Security = append(operation.Security, securityMap)
  638. return nil
  639. }
  640. // findTypeDef attempts to find the *ast.TypeSpec for a specific type given the
  641. // type's name and the package's import path.
  642. // TODO: improve finding external pkg.
  643. func findTypeDef(importPath, typeName string) (*ast.TypeSpec, error) {
  644. cwd, err := os.Getwd()
  645. if err != nil {
  646. return nil, err
  647. }
  648. conf := loader.Config{
  649. ParserMode: goparser.SpuriousErrors,
  650. Cwd: cwd,
  651. }
  652. conf.Import(importPath)
  653. lprog, err := conf.Load()
  654. if err != nil {
  655. return nil, err
  656. }
  657. // If the pkg is vendored, the actual pkg path is going to resemble
  658. // something like "{importPath}/vendor/{importPath}"
  659. for k := range lprog.AllPackages {
  660. realPkgPath := k.Path()
  661. if strings.Contains(realPkgPath, "vendor/"+importPath) {
  662. importPath = realPkgPath
  663. }
  664. }
  665. pkgInfo := lprog.Package(importPath)
  666. if pkgInfo == nil {
  667. return nil, fmt.Errorf("package was nil")
  668. }
  669. // TODO: possibly cache pkgInfo since it's an expensive operation
  670. for i := range pkgInfo.Files {
  671. for _, astDeclaration := range pkgInfo.Files[i].Decls {
  672. generalDeclaration, ok := astDeclaration.(*ast.GenDecl)
  673. if ok && generalDeclaration.Tok == token.TYPE {
  674. for _, astSpec := range generalDeclaration.Specs {
  675. typeSpec, ok := astSpec.(*ast.TypeSpec)
  676. if ok {
  677. if typeSpec.Name.String() == typeName {
  678. return typeSpec, nil
  679. }
  680. }
  681. }
  682. }
  683. }
  684. }
  685. return nil, fmt.Errorf("type spec not found")
  686. }
  687. var responsePattern = regexp.MustCompile(`^([\w,]+)\s+([\w{}]+)\s+([\w\-.\\{}=,\[\s\]]+)\s*(".*)?`)
  688. // ResponseType{data1=Type1,data2=Type2}.
  689. var combinedPattern = regexp.MustCompile(`^([\w\-./\[\]]+){(.*)}$`)
  690. func (operation *Operation) parseObjectSchema(refType string, astFile *ast.File) (*spec.Schema, error) {
  691. return parseObjectSchema(operation.parser, refType, astFile)
  692. }
  693. func parseObjectSchema(parser *Parser, refType string, astFile *ast.File) (*spec.Schema, error) {
  694. switch {
  695. case refType == NIL:
  696. return nil, nil
  697. case refType == INTERFACE:
  698. return PrimitiveSchema(OBJECT), nil
  699. case refType == ANY:
  700. return PrimitiveSchema(OBJECT), nil
  701. case IsGolangPrimitiveType(refType):
  702. refType = TransToValidSchemeType(refType)
  703. return PrimitiveSchema(refType), nil
  704. case IsPrimitiveType(refType):
  705. return PrimitiveSchema(refType), nil
  706. case strings.HasPrefix(refType, "[]"):
  707. schema, err := parseObjectSchema(parser, refType[2:], astFile)
  708. if err != nil {
  709. return nil, err
  710. }
  711. return spec.ArrayProperty(schema), nil
  712. case strings.HasPrefix(refType, "map["):
  713. // ignore key type
  714. idx := strings.Index(refType, "]")
  715. if idx < 0 {
  716. return nil, fmt.Errorf("invalid type: %s", refType)
  717. }
  718. refType = refType[idx+1:]
  719. if refType == INTERFACE || refType == ANY {
  720. return spec.MapProperty(nil), nil
  721. }
  722. schema, err := parseObjectSchema(parser, refType, astFile)
  723. if err != nil {
  724. return nil, err
  725. }
  726. return spec.MapProperty(schema), nil
  727. case strings.Contains(refType, "{"):
  728. return parseCombinedObjectSchema(parser, refType, astFile)
  729. default:
  730. if parser != nil { // checking refType has existing in 'TypeDefinitions'
  731. schema, err := parser.getTypeSchema(refType, astFile, true)
  732. if err != nil {
  733. return nil, err
  734. }
  735. return schema, nil
  736. }
  737. return RefSchema(refType), nil
  738. }
  739. }
  740. func parseFields(s string) []string {
  741. nestLevel := 0
  742. return strings.FieldsFunc(s, func(char rune) bool {
  743. if char == '{' {
  744. nestLevel++
  745. return false
  746. } else if char == '}' {
  747. nestLevel--
  748. return false
  749. }
  750. return char == ',' && nestLevel == 0
  751. })
  752. }
  753. func parseCombinedObjectSchema(parser *Parser, refType string, astFile *ast.File) (*spec.Schema, error) {
  754. matches := combinedPattern.FindStringSubmatch(refType)
  755. if len(matches) != 3 {
  756. return nil, fmt.Errorf("invalid type: %s", refType)
  757. }
  758. schema, err := parseObjectSchema(parser, matches[1], astFile)
  759. if err != nil {
  760. return nil, err
  761. }
  762. fields, props := parseFields(matches[2]), map[string]spec.Schema{}
  763. for _, field := range fields {
  764. keyVal := strings.SplitN(field, "=", 2)
  765. if len(keyVal) == 2 {
  766. schema, err := parseObjectSchema(parser, keyVal[1], astFile)
  767. if err != nil {
  768. return nil, err
  769. }
  770. props[keyVal[0]] = *schema
  771. }
  772. }
  773. if len(props) == 0 {
  774. return schema, nil
  775. }
  776. return spec.ComposedSchema(*schema, spec.Schema{
  777. SchemaProps: spec.SchemaProps{
  778. Type: []string{OBJECT},
  779. Properties: props,
  780. },
  781. }), nil
  782. }
  783. func (operation *Operation) parseAPIObjectSchema(commentLine, schemaType, refType string, astFile *ast.File) (*spec.Schema, error) {
  784. if strings.HasSuffix(refType, ",") && strings.Contains(refType, "[") {
  785. // regexp may have broken generics syntax. find closing bracket and add it back
  786. allMatchesLenOffset := strings.Index(commentLine, refType) + len(refType)
  787. lostPartEndIdx := strings.Index(commentLine[allMatchesLenOffset:], "]")
  788. if lostPartEndIdx >= 0 {
  789. refType += commentLine[allMatchesLenOffset : allMatchesLenOffset+lostPartEndIdx+1]
  790. }
  791. }
  792. switch schemaType {
  793. case OBJECT:
  794. if !strings.HasPrefix(refType, "[]") {
  795. return operation.parseObjectSchema(refType, astFile)
  796. }
  797. refType = refType[2:]
  798. fallthrough
  799. case ARRAY:
  800. schema, err := operation.parseObjectSchema(refType, astFile)
  801. if err != nil {
  802. return nil, err
  803. }
  804. return spec.ArrayProperty(schema), nil
  805. default:
  806. return PrimitiveSchema(schemaType), nil
  807. }
  808. }
  809. // ParseResponseComment parses comment for given `response` comment string.
  810. func (operation *Operation) ParseResponseComment(commentLine string, astFile *ast.File) error {
  811. matches := responsePattern.FindStringSubmatch(commentLine)
  812. if len(matches) != 5 {
  813. err := operation.ParseEmptyResponseComment(commentLine)
  814. if err != nil {
  815. return operation.ParseEmptyResponseOnly(commentLine)
  816. }
  817. return err
  818. }
  819. description := strings.Trim(matches[4], "\"")
  820. schema, err := operation.parseAPIObjectSchema(commentLine, strings.Trim(matches[2], "{}"), strings.TrimSpace(matches[3]), astFile)
  821. if err != nil {
  822. return err
  823. }
  824. for _, codeStr := range strings.Split(matches[1], ",") {
  825. if strings.EqualFold(codeStr, defaultTag) {
  826. operation.DefaultResponse().WithSchema(schema).WithDescription(description)
  827. continue
  828. }
  829. code, err := strconv.Atoi(codeStr)
  830. if err != nil {
  831. return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
  832. }
  833. resp := spec.NewResponse().WithSchema(schema).WithDescription(description)
  834. if description == "" {
  835. resp.WithDescription(http.StatusText(code))
  836. }
  837. operation.AddResponse(code, resp)
  838. }
  839. return nil
  840. }
  841. func newHeaderSpec(schemaType, description string) spec.Header {
  842. return spec.Header{
  843. SimpleSchema: spec.SimpleSchema{
  844. Type: schemaType,
  845. },
  846. HeaderProps: spec.HeaderProps{
  847. Description: description,
  848. },
  849. VendorExtensible: spec.VendorExtensible{
  850. Extensions: nil,
  851. },
  852. CommonValidations: spec.CommonValidations{
  853. Maximum: nil,
  854. ExclusiveMaximum: false,
  855. Minimum: nil,
  856. ExclusiveMinimum: false,
  857. MaxLength: nil,
  858. MinLength: nil,
  859. Pattern: "",
  860. MaxItems: nil,
  861. MinItems: nil,
  862. UniqueItems: false,
  863. MultipleOf: nil,
  864. Enum: nil,
  865. },
  866. }
  867. }
  868. // ParseResponseHeaderComment parses comment for given `response header` comment string.
  869. func (operation *Operation) ParseResponseHeaderComment(commentLine string, _ *ast.File) error {
  870. matches := responsePattern.FindStringSubmatch(commentLine)
  871. if len(matches) != 5 {
  872. return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
  873. }
  874. header := newHeaderSpec(strings.Trim(matches[2], "{}"), strings.Trim(matches[4], "\""))
  875. headerKey := strings.TrimSpace(matches[3])
  876. if strings.EqualFold(matches[1], "all") {
  877. if operation.Responses.Default != nil {
  878. operation.Responses.Default.Headers[headerKey] = header
  879. }
  880. if operation.Responses.StatusCodeResponses != nil {
  881. for code, response := range operation.Responses.StatusCodeResponses {
  882. response.Headers[headerKey] = header
  883. operation.Responses.StatusCodeResponses[code] = response
  884. }
  885. }
  886. return nil
  887. }
  888. for _, codeStr := range strings.Split(matches[1], ",") {
  889. if strings.EqualFold(codeStr, defaultTag) {
  890. if operation.Responses.Default != nil {
  891. operation.Responses.Default.Headers[headerKey] = header
  892. }
  893. continue
  894. }
  895. code, err := strconv.Atoi(codeStr)
  896. if err != nil {
  897. return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
  898. }
  899. if operation.Responses.StatusCodeResponses != nil {
  900. response, responseExist := operation.Responses.StatusCodeResponses[code]
  901. if responseExist {
  902. response.Headers[headerKey] = header
  903. operation.Responses.StatusCodeResponses[code] = response
  904. }
  905. }
  906. }
  907. return nil
  908. }
  909. var emptyResponsePattern = regexp.MustCompile(`([\w,]+)\s+"(.*)"`)
  910. // ParseEmptyResponseComment parse only comment out status code and description,eg: @Success 200 "it's ok".
  911. func (operation *Operation) ParseEmptyResponseComment(commentLine string) error {
  912. matches := emptyResponsePattern.FindStringSubmatch(commentLine)
  913. if len(matches) != 3 {
  914. return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
  915. }
  916. description := strings.Trim(matches[2], "\"")
  917. for _, codeStr := range strings.Split(matches[1], ",") {
  918. if strings.EqualFold(codeStr, defaultTag) {
  919. operation.DefaultResponse().WithDescription(description)
  920. continue
  921. }
  922. code, err := strconv.Atoi(codeStr)
  923. if err != nil {
  924. return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
  925. }
  926. operation.AddResponse(code, spec.NewResponse().WithDescription(description))
  927. }
  928. return nil
  929. }
  930. // ParseEmptyResponseOnly parse only comment out status code ,eg: @Success 200.
  931. func (operation *Operation) ParseEmptyResponseOnly(commentLine string) error {
  932. for _, codeStr := range strings.Split(commentLine, ",") {
  933. if strings.EqualFold(codeStr, defaultTag) {
  934. _ = operation.DefaultResponse()
  935. continue
  936. }
  937. code, err := strconv.Atoi(codeStr)
  938. if err != nil {
  939. return fmt.Errorf("can not parse response comment \"%s\"", commentLine)
  940. }
  941. operation.AddResponse(code, spec.NewResponse().WithDescription(http.StatusText(code)))
  942. }
  943. return nil
  944. }
  945. // DefaultResponse return the default response member pointer.
  946. func (operation *Operation) DefaultResponse() *spec.Response {
  947. if operation.Responses.Default == nil {
  948. operation.Responses.Default = &spec.Response{
  949. ResponseProps: spec.ResponseProps{
  950. Description: "",
  951. Headers: make(map[string]spec.Header),
  952. },
  953. }
  954. }
  955. return operation.Responses.Default
  956. }
  957. // AddResponse add a response for a code.
  958. func (operation *Operation) AddResponse(code int, response *spec.Response) {
  959. if response.Headers == nil {
  960. response.Headers = make(map[string]spec.Header)
  961. }
  962. operation.Responses.StatusCodeResponses[code] = *response
  963. }
  964. // createParameter returns swagger spec.Parameter for given paramType, description, paramName, schemaType, required.
  965. func createParameter(paramType, description, paramName, schemaType string, required bool) spec.Parameter {
  966. // //five possible parameter types. query, path, body, header, form
  967. result := spec.Parameter{
  968. ParamProps: spec.ParamProps{
  969. Name: paramName,
  970. Description: description,
  971. Required: required,
  972. In: paramType,
  973. Schema: nil,
  974. AllowEmptyValue: false,
  975. },
  976. }
  977. if paramType == "body" {
  978. result.ParamProps.Schema = &spec.Schema{
  979. SchemaProps: spec.SchemaProps{
  980. Type: []string{schemaType},
  981. },
  982. }
  983. return result
  984. }
  985. result.SimpleSchema = spec.SimpleSchema{
  986. Type: schemaType,
  987. Nullable: false,
  988. Format: "",
  989. }
  990. return result
  991. }
  992. func getCodeExampleForSummary(summaryName string, dirPath string) ([]byte, error) {
  993. dirEntries, err := os.ReadDir(dirPath)
  994. if err != nil {
  995. return nil, err
  996. }
  997. for _, entry := range dirEntries {
  998. if entry.IsDir() {
  999. continue
  1000. }
  1001. fileName := entry.Name()
  1002. if !strings.Contains(fileName, ".json") {
  1003. continue
  1004. }
  1005. if strings.Contains(fileName, summaryName) {
  1006. fullPath := filepath.Join(dirPath, fileName)
  1007. commentInfo, err := os.ReadFile(fullPath)
  1008. if err != nil {
  1009. return nil, fmt.Errorf("Failed to read code example file %s error: %s ", fullPath, err)
  1010. }
  1011. return commentInfo, nil
  1012. }
  1013. }
  1014. return nil, fmt.Errorf("unable to find code example file for tag %s in the given directory", summaryName)
  1015. }