github.com/rzurga/go-swagger@v0.28.1-0.20211109195225-5d1f453ffa3a/scan/scanner.go (about) 1 // +build !go1.11 2 3 // Copyright 2015 go-swagger maintainers 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package scan 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "go/ast" 24 "go/build" 25 goparser "go/parser" 26 "go/types" 27 "log" 28 "os" 29 "regexp" 30 "strings" 31 32 "github.com/go-openapi/loads/fmts" 33 "github.com/go-openapi/spec" 34 "github.com/go-openapi/swag" 35 "golang.org/x/tools/go/loader" 36 yaml "gopkg.in/yaml.v2" 37 ) 38 39 const ( 40 rxMethod = "(\\p{L}+)" 41 rxPath = "((?:/[\\p{L}\\p{N}\\p{Pd}\\p{Pc}{}\\-\\.\\?_~%!$&'()*+,;=:@/]*)+/?)" 42 rxOpTags = "(\\p{L}[\\p{L}\\p{N}\\p{Pd}\\.\\p{Pc}\\p{Zs}]+)" 43 rxOpID = "((?:\\p{L}[\\p{L}\\p{N}\\p{Pd}\\p{Pc}]+)+)" 44 45 rxMaximumFmt = "%s[Mm]ax(?:imum)?\\p{Zs}*:\\p{Zs}*([\\<=])?\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$" 46 rxMinimumFmt = "%s[Mm]in(?:imum)?\\p{Zs}*:\\p{Zs}*([\\>=])?\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$" 47 rxMultipleOfFmt = "%s[Mm]ultiple\\p{Zs}*[Oo]f\\p{Zs}*:\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$" 48 49 rxMaxLengthFmt = "%s[Mm]ax(?:imum)?(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ll]en(?:gth)?)\\p{Zs}*:\\p{Zs}*(\\p{N}+)$" 50 rxMinLengthFmt = "%s[Mm]in(?:imum)?(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ll]en(?:gth)?)\\p{Zs}*:\\p{Zs}*(\\p{N}+)$" 51 rxPatternFmt = "%s[Pp]attern\\p{Zs}*:\\p{Zs}*(.*)$" 52 rxCollectionFormatFmt = "%s[Cc]ollection(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ff]ormat)\\p{Zs}*:\\p{Zs}*(.*)$" 53 rxEnumFmt = "%s[Ee]num\\p{Zs}*:\\p{Zs}*(.*)$" 54 rxDefaultFmt = "%s[Dd]efault\\p{Zs}*:\\p{Zs}*(.*)$" 55 rxExampleFmt = "%s[Ee]xample\\p{Zs}*:\\p{Zs}*(.*)$" 56 57 rxMaxItemsFmt = "%s[Mm]ax(?:imum)?(?:\\p{Zs}*|[\\p{Pd}\\p{Pc}]|\\.)?[Ii]tems\\p{Zs}*:\\p{Zs}*(\\p{N}+)$" 58 rxMinItemsFmt = "%s[Mm]in(?:imum)?(?:\\p{Zs}*|[\\p{Pd}\\p{Pc}]|\\.)?[Ii]tems\\p{Zs}*:\\p{Zs}*(\\p{N}+)$" 59 rxUniqueFmt = "%s[Uu]nique\\p{Zs}*:\\p{Zs}*(true|false)$" 60 61 rxItemsPrefixFmt = "(?:[Ii]tems[\\.\\p{Zs}]*){%d}" 62 ) 63 64 var ( 65 rxSwaggerAnnotation = regexp.MustCompile(`swagger:([\p{L}\p{N}\p{Pd}\p{Pc}]+)`) 66 rxFileUpload = regexp.MustCompile(`swagger:file`) 67 rxStrFmt = regexp.MustCompile(`swagger:strfmt\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`) 68 rxAlias = regexp.MustCompile(`swagger:alias`) 69 rxName = regexp.MustCompile(`swagger:name\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\.]+)$`) 70 rxAllOf = regexp.MustCompile(`swagger:allOf\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\.]+)?$`) 71 rxModelOverride = regexp.MustCompile(`swagger:model\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`) 72 rxResponseOverride = regexp.MustCompile(`swagger:response\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`) 73 rxParametersOverride = regexp.MustCompile(`swagger:parameters\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\p{Zs}]+)$`) 74 rxEnum = regexp.MustCompile(`swagger:enum\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`) 75 rxIgnoreOverride = regexp.MustCompile(`swagger:ignore\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`) 76 rxDefault = regexp.MustCompile(`swagger:default\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`) 77 rxType = regexp.MustCompile(`swagger:type\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`) 78 rxRoute = regexp.MustCompile( 79 "swagger:route\\p{Zs}*" + 80 rxMethod + 81 "\\p{Zs}*" + 82 rxPath + 83 "(?:\\p{Zs}+" + 84 rxOpTags + 85 ")?\\p{Zs}+" + 86 rxOpID + "\\p{Zs}*$") 87 rxBeginYAMLSpec = regexp.MustCompile(`---\p{Zs}*$`) 88 rxUncommentHeaders = regexp.MustCompile(`^[\p{Zs}\t/\*-]*\|?`) 89 rxUncommentYAML = regexp.MustCompile(`^[\p{Zs}\t]*/*`) 90 rxOperation = regexp.MustCompile( 91 "swagger:operation\\p{Zs}*" + 92 rxMethod + 93 "\\p{Zs}*" + 94 rxPath + 95 "(?:\\p{Zs}+" + 96 rxOpTags + 97 ")?\\p{Zs}+" + 98 rxOpID + "\\p{Zs}*$") 99 100 rxSpace = regexp.MustCompile(`\p{Zs}+`) 101 rxIndent = regexp.MustCompile(`\p{Zs}*/*\p{Zs}*[^\p{Zs}]`) 102 rxPunctuationEnd = regexp.MustCompile(`\p{Po}$`) 103 rxStripComments = regexp.MustCompile(`^[^\p{L}\p{N}\p{Pd}\p{Pc}\+]*`) 104 rxStripTitleComments = regexp.MustCompile(`^[^\p{L}]*[Pp]ackage\p{Zs}+[^\p{Zs}]+\p{Zs}*`) 105 rxAllowedExtensions = regexp.MustCompile(`^[Xx]-`) 106 107 rxIn = regexp.MustCompile(`[Ii]n\p{Zs}*:\p{Zs}*(query|path|header|body|formData)$`) 108 rxRequired = regexp.MustCompile(`[Rr]equired\p{Zs}*:\p{Zs}*(true|false)$`) 109 rxDiscriminator = regexp.MustCompile(`[Dd]iscriminator\p{Zs}*:\p{Zs}*(true|false)$`) 110 rxReadOnly = regexp.MustCompile(`[Rr]ead(?:\p{Zs}*|[\p{Pd}\p{Pc}])?[Oo]nly\p{Zs}*:\p{Zs}*(true|false)$`) 111 rxConsumes = regexp.MustCompile(`[Cc]onsumes\p{Zs}*:`) 112 rxProduces = regexp.MustCompile(`[Pp]roduces\p{Zs}*:`) 113 rxSecuritySchemes = regexp.MustCompile(`[Ss]ecurity\p{Zs}*:`) 114 rxSecurity = regexp.MustCompile(`[Ss]ecurity\p{Zs}*[Dd]efinitions:`) 115 rxResponses = regexp.MustCompile(`[Rr]esponses\p{Zs}*:`) 116 rxParameters = regexp.MustCompile(`[Pp]arameters\p{Zs}*:`) 117 rxSchemes = regexp.MustCompile(`[Ss]chemes\p{Zs}*:\p{Zs}*((?:(?:https?|HTTPS?|wss?|WSS?)[\p{Zs},]*)+)$`) 118 rxVersion = regexp.MustCompile(`[Vv]ersion\p{Zs}*:\p{Zs}*(.+)$`) 119 rxHost = regexp.MustCompile(`[Hh]ost\p{Zs}*:\p{Zs}*(.+)$`) 120 rxBasePath = regexp.MustCompile(`[Bb]ase\p{Zs}*-*[Pp]ath\p{Zs}*:\p{Zs}*` + rxPath + "$") 121 rxLicense = regexp.MustCompile(`[Ll]icense\p{Zs}*:\p{Zs}*(.+)$`) 122 rxContact = regexp.MustCompile(`[Cc]ontact\p{Zs}*-?(?:[Ii]info\p{Zs}*)?:\p{Zs}*(.+)$`) 123 rxTOS = regexp.MustCompile(`[Tt](:?erms)?\p{Zs}*-?[Oo]f?\p{Zs}*-?[Ss](?:ervice)?\p{Zs}*:`) 124 rxExtensions = regexp.MustCompile(`[Ee]xtensions\p{Zs}*:`) 125 rxInfoExtensions = regexp.MustCompile(`[In]nfo\p{Zs}*[Ee]xtensions:`) 126 // currently unused: rxExample = regexp.MustCompile(`[Ex]ample\p{Zs}*:\p{Zs}*(.*)$`) 127 ) 128 129 // Many thanks go to https://github.com/yvasiyarov/swagger 130 // this is loosely based on that implementation but for swagger 2.0 131 132 func joinDropLast(lines []string) string { 133 l := len(lines) 134 lns := lines 135 if l > 0 && len(strings.TrimSpace(lines[l-1])) == 0 { 136 lns = lines[:l-1] 137 } 138 return strings.Join(lns, "\n") 139 } 140 141 func removeEmptyLines(lines []string) (notEmpty []string) { 142 for _, l := range lines { 143 if len(strings.TrimSpace(l)) > 0 { 144 notEmpty = append(notEmpty, l) 145 } 146 } 147 return 148 } 149 150 func rxf(rxp, ar string) *regexp.Regexp { 151 return regexp.MustCompile(fmt.Sprintf(rxp, ar)) 152 } 153 154 // The Opts for the application scanner. 155 type Opts struct { 156 BasePath string 157 Input *spec.Swagger 158 ScanModels bool 159 BuildTags string 160 Include []string 161 Exclude []string 162 IncludeTags []string 163 ExcludeTags []string 164 } 165 166 func safeConvert(str string) bool { 167 b, err := swag.ConvertBool(str) 168 if err != nil { 169 return false 170 } 171 return b 172 } 173 174 // Debug is true when process is run with DEBUG=1 env var 175 var Debug = safeConvert(os.Getenv("DEBUG")) 176 177 // Application scans the application and builds a swagger spec based on the information from the code files. 178 // When there are includes provided, only those files are considered for the initial discovery. 179 // Similarly the excludes will exclude an item from initial discovery through scanning for annotations. 180 // When something in the discovered items requires a type that is contained in the includes or excludes it will still be 181 // in the spec. 182 func Application(opts Opts) (*spec.Swagger, error) { 183 parser, err := newAppScanner(&opts) 184 185 if err != nil { 186 return nil, err 187 } 188 return parser.Parse() 189 } 190 191 // appScanner the global context for scanning a go application 192 // into a swagger specification 193 type appScanner struct { 194 loader *loader.Config 195 prog *loader.Program 196 classifier *programClassifier 197 discovered []schemaDecl 198 input *spec.Swagger 199 definitions map[string]spec.Schema 200 responses map[string]spec.Response 201 operations map[string]*spec.Operation 202 scanModels bool 203 includeTags map[string]bool 204 excludeTas map[string]bool 205 206 // MainPackage the path to find the main class in 207 MainPackage string 208 } 209 210 // newAppScanner creates a new api parser 211 func newAppScanner(opts *Opts) (*appScanner, error) { 212 if Debug { 213 log.Println("scanning packages discovered through entrypoint @ ", opts.BasePath) 214 } 215 var ldr loader.Config 216 ldr.ParserMode = goparser.ParseComments 217 ldr.Import(opts.BasePath) 218 if opts.BuildTags != "" { 219 ldr.Build = &build.Default 220 ldr.Build.BuildTags = strings.Split(opts.BuildTags, ",") 221 } 222 ldr.TypeChecker = types.Config{FakeImportC: true} 223 prog, err := ldr.Load() 224 if err != nil { 225 return nil, err 226 } 227 228 var includes, excludes packageFilters 229 if len(opts.Include) > 0 { 230 for _, include := range opts.Include { 231 includes = append(includes, packageFilter{Name: include}) 232 } 233 } 234 if len(opts.Exclude) > 0 { 235 for _, exclude := range opts.Exclude { 236 excludes = append(excludes, packageFilter{Name: exclude}) 237 } 238 } 239 includeTags := make(map[string]bool) 240 for _, includeTag := range opts.IncludeTags { 241 includeTags[includeTag] = true 242 } 243 excludeTags := make(map[string]bool) 244 for _, excludeTag := range opts.ExcludeTags { 245 excludeTags[excludeTag] = true 246 } 247 248 input := opts.Input 249 if input == nil { 250 input = new(spec.Swagger) 251 input.Swagger = "2.0" 252 } 253 254 if input.Paths == nil { 255 input.Paths = new(spec.Paths) 256 } 257 if input.Definitions == nil { 258 input.Definitions = make(map[string]spec.Schema) 259 } 260 if input.Responses == nil { 261 input.Responses = make(map[string]spec.Response) 262 } 263 if input.Extensions == nil { 264 input.Extensions = make(spec.Extensions) 265 } 266 267 return &appScanner{ 268 MainPackage: opts.BasePath, 269 prog: prog, 270 input: input, 271 loader: &ldr, 272 operations: collectOperationsFromInput(input), 273 definitions: input.Definitions, 274 responses: input.Responses, 275 scanModels: opts.ScanModels, 276 classifier: &programClassifier{ 277 Includes: includes, 278 Excludes: excludes, 279 }, 280 includeTags: includeTags, 281 excludeTas: excludeTags, 282 }, nil 283 } 284 285 func collectOperationsFromInput(input *spec.Swagger) map[string]*spec.Operation { 286 operations := make(map[string]*spec.Operation) 287 if input != nil && input.Paths != nil { 288 for _, pth := range input.Paths.Paths { 289 if pth.Get != nil { 290 operations[pth.Get.ID] = pth.Get 291 } 292 if pth.Post != nil { 293 operations[pth.Post.ID] = pth.Post 294 } 295 if pth.Put != nil { 296 operations[pth.Put.ID] = pth.Put 297 } 298 if pth.Patch != nil { 299 operations[pth.Patch.ID] = pth.Patch 300 } 301 if pth.Delete != nil { 302 operations[pth.Delete.ID] = pth.Delete 303 } 304 if pth.Head != nil { 305 operations[pth.Head.ID] = pth.Head 306 } 307 if pth.Options != nil { 308 operations[pth.Options.ID] = pth.Options 309 } 310 } 311 } 312 return operations 313 } 314 315 // Parse produces a swagger object for an application 316 func (a *appScanner) Parse() (*spec.Swagger, error) { 317 // classification still includes files that are completely commented out 318 cp, err := a.classifier.Classify(a.prog) 319 if err != nil { 320 return nil, err 321 } 322 323 // build models dictionary 324 if a.scanModels { 325 for _, modelsFile := range cp.Models { 326 if err := a.parseSchema(modelsFile); err != nil { 327 return nil, err 328 } 329 } 330 } 331 332 // build parameters dictionary 333 for _, paramsFile := range cp.Parameters { 334 if err := a.parseParameters(paramsFile); err != nil { 335 return nil, err 336 } 337 } 338 339 // build responses dictionary 340 for _, responseFile := range cp.Responses { 341 if err := a.parseResponses(responseFile); err != nil { 342 return nil, err 343 } 344 } 345 346 // build definitions dictionary 347 if err := a.processDiscovered(); err != nil { 348 return nil, err 349 } 350 351 // build paths dictionary 352 for _, routeFile := range cp.Routes { 353 if err := a.parseRoutes(routeFile); err != nil { 354 return nil, err 355 } 356 } 357 for _, operationFile := range cp.Operations { 358 if err := a.parseOperations(operationFile); err != nil { 359 return nil, err 360 } 361 } 362 363 // build swagger object 364 for _, metaFile := range cp.Meta { 365 if err := a.parseMeta(metaFile); err != nil { 366 return nil, err 367 } 368 } 369 370 if a.input.Swagger == "" { 371 a.input.Swagger = "2.0" 372 } 373 374 return a.input, nil 375 } 376 377 func (a *appScanner) processDiscovered() error { 378 // loop over discovered until all the items are in definitions 379 keepGoing := len(a.discovered) > 0 380 for keepGoing { 381 var queue []schemaDecl 382 for _, d := range a.discovered { 383 if _, ok := a.definitions[d.Name]; !ok { 384 queue = append(queue, d) 385 } 386 } 387 a.discovered = nil 388 for _, sd := range queue { 389 if err := a.parseDiscoveredSchema(sd); err != nil { 390 return err 391 } 392 } 393 keepGoing = len(a.discovered) > 0 394 } 395 396 return nil 397 } 398 399 func (a *appScanner) parseSchema(file *ast.File) error { 400 sp := newSchemaParser(a.prog) 401 if err := sp.Parse(file, a.definitions); err != nil { 402 return err 403 } 404 a.discovered = append(a.discovered, sp.postDecls...) 405 return nil 406 } 407 408 func (a *appScanner) parseDiscoveredSchema(sd schemaDecl) error { 409 sp := newSchemaParser(a.prog) 410 sp.discovered = &sd 411 412 if err := sp.Parse(sd.File, a.definitions); err != nil { 413 return err 414 } 415 a.discovered = append(a.discovered, sp.postDecls...) 416 return nil 417 } 418 419 func (a *appScanner) parseRoutes(file *ast.File) error { 420 rp := newRoutesParser(a.prog) 421 rp.operations = a.operations 422 rp.definitions = a.definitions 423 rp.responses = a.responses 424 425 return rp.Parse(file, a.input.Paths, a.includeTags, a.excludeTas) 426 } 427 428 func (a *appScanner) parseOperations(file *ast.File) error { 429 op := newOperationsParser(a.prog) 430 op.operations = a.operations 431 op.definitions = a.definitions 432 op.responses = a.responses 433 return op.Parse(file, a.input.Paths, a.includeTags, a.excludeTas) 434 } 435 436 func (a *appScanner) parseParameters(file *ast.File) error { 437 rp := newParameterParser(a.prog) 438 if err := rp.Parse(file, a.operations); err != nil { 439 return err 440 } 441 a.discovered = append(a.discovered, rp.postDecls...) 442 a.discovered = append(a.discovered, rp.scp.postDecls...) 443 return nil 444 } 445 446 func (a *appScanner) parseResponses(file *ast.File) error { 447 rp := newResponseParser(a.prog) 448 if err := rp.Parse(file, a.responses); err != nil { 449 return err 450 } 451 a.discovered = append(a.discovered, rp.postDecls...) 452 a.discovered = append(a.discovered, rp.scp.postDecls...) 453 return nil 454 } 455 456 func (a *appScanner) parseMeta(file *ast.File) error { 457 return newMetaParser(a.input).Parse(file.Doc) 458 } 459 460 // MustExpandPackagePath gets the real package path on disk 461 func (a *appScanner) MustExpandPackagePath(packagePath string) string { 462 pkgRealpath := swag.FindInGoSearchPath(packagePath) 463 if pkgRealpath == "" { 464 log.Fatalf("Can't find package %s \n", packagePath) 465 } 466 467 return pkgRealpath 468 } 469 470 type swaggerTypable interface { 471 Typed(string, string) 472 SetRef(spec.Ref) 473 Items() swaggerTypable 474 WithEnum(...interface{}) 475 Schema() *spec.Schema 476 Level() int 477 } 478 479 // Map all Go builtin types that have Json representation to Swagger/Json types. 480 // See https://golang.org/pkg/builtin/ and http://swagger.io/specification/ 481 func swaggerSchemaForType(typeName string, prop swaggerTypable) error { 482 switch typeName { 483 case "bool": 484 prop.Typed("boolean", "") 485 case "byte": 486 prop.Typed("integer", "uint8") 487 case "complex128", "complex64": 488 return fmt.Errorf("unsupported builtin %q (no JSON marshaller)", typeName) 489 case "error": 490 // TODO: error is often marshalled into a string but not always (e.g. errors package creates 491 // errors that are marshalled into an empty object), this could be handled the same way 492 // custom JSON marshallers are handled (in future) 493 prop.Typed("string", "") 494 case "float32": 495 prop.Typed("number", "float") 496 case "float64": 497 prop.Typed("number", "double") 498 case "int": 499 prop.Typed("integer", "int64") 500 case "int16": 501 prop.Typed("integer", "int16") 502 case "int32": 503 prop.Typed("integer", "int32") 504 case "int64": 505 prop.Typed("integer", "int64") 506 case "int8": 507 prop.Typed("integer", "int8") 508 case "rune": 509 prop.Typed("integer", "int32") 510 case "string": 511 prop.Typed("string", "") 512 case "uint": 513 prop.Typed("integer", "uint64") 514 case "uint16": 515 prop.Typed("integer", "uint16") 516 case "uint32": 517 prop.Typed("integer", "uint32") 518 case "uint64": 519 prop.Typed("integer", "uint64") 520 case "uint8": 521 prop.Typed("integer", "uint8") 522 case "uintptr": 523 prop.Typed("integer", "uint64") 524 default: 525 return fmt.Errorf("unsupported type %q", typeName) 526 } 527 return nil 528 } 529 530 func newMultiLineTagParser(name string, parser valueParser, skipCleanUp bool) tagParser { 531 return tagParser{ 532 Name: name, 533 MultiLine: true, 534 SkipCleanUp: skipCleanUp, 535 Parser: parser, 536 } 537 } 538 539 func newSingleLineTagParser(name string, parser valueParser) tagParser { 540 return tagParser{ 541 Name: name, 542 MultiLine: false, 543 SkipCleanUp: false, 544 Parser: parser, 545 } 546 } 547 548 type tagParser struct { 549 Name string 550 MultiLine bool 551 SkipCleanUp bool 552 Lines []string 553 Parser valueParser 554 } 555 556 func (st *tagParser) Matches(line string) bool { 557 return st.Parser.Matches(line) 558 } 559 560 func (st *tagParser) Parse(lines []string) error { 561 return st.Parser.Parse(lines) 562 } 563 564 func newYamlParser(rx *regexp.Regexp, setter func(json.RawMessage) error) valueParser { 565 return &yamlParser{ 566 set: setter, 567 rx: rx, 568 } 569 } 570 571 type yamlParser struct { 572 set func(json.RawMessage) error 573 rx *regexp.Regexp 574 } 575 576 func (y *yamlParser) Parse(lines []string) error { 577 if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { 578 return nil 579 } 580 581 var uncommented []string 582 uncommented = append(uncommented, removeYamlIndent(lines)...) 583 584 yamlContent := strings.Join(uncommented, "\n") 585 var yamlValue interface{} 586 err := yaml.Unmarshal([]byte(yamlContent), &yamlValue) 587 if err != nil { 588 return err 589 } 590 591 var jsonValue json.RawMessage 592 jsonValue, err = fmts.YAMLToJSON(yamlValue) 593 if err != nil { 594 return err 595 } 596 597 return y.set(jsonValue) 598 } 599 600 func (y *yamlParser) Matches(line string) bool { 601 return y.rx.MatchString(line) 602 } 603 604 // aggregates lines in header until it sees `---`, 605 // the beginning of a YAML spec 606 type yamlSpecScanner struct { 607 header []string 608 yamlSpec []string 609 setTitle func([]string) 610 setDescription func([]string) 611 workedOutTitle bool 612 title []string 613 skipHeader bool 614 } 615 616 func cleanupScannerLines(lines []string, ur *regexp.Regexp, yamlBlock *regexp.Regexp) []string { 617 // bail early when there is nothing to parse 618 if len(lines) == 0 { 619 return lines 620 } 621 seenLine := -1 622 var lastContent int 623 var uncommented []string 624 var startBlock bool 625 var yaml []string 626 for i, v := range lines { 627 if yamlBlock != nil && yamlBlock.MatchString(v) && !startBlock { 628 startBlock = true 629 if seenLine < 0 { 630 seenLine = i 631 } 632 continue 633 } 634 if startBlock { 635 if yamlBlock.MatchString(v) { 636 startBlock = false 637 uncommented = append(uncommented, removeIndent(yaml)...) 638 continue 639 } 640 yaml = append(yaml, v) 641 if v != "" { 642 if seenLine < 0 { 643 seenLine = i 644 } 645 lastContent = i 646 } 647 continue 648 } 649 str := ur.ReplaceAllString(v, "") 650 uncommented = append(uncommented, str) 651 if str != "" { 652 if seenLine < 0 { 653 seenLine = i 654 } 655 lastContent = i 656 } 657 } 658 659 // fixes issue #50 660 if seenLine == -1 { 661 return nil 662 } 663 return uncommented[seenLine : lastContent+1] 664 } 665 666 // a shared function that can be used to split given headers 667 // into a title and description 668 func collectScannerTitleDescription(headers []string) (title, desc []string) { 669 hdrs := cleanupScannerLines(headers, rxUncommentHeaders, nil) 670 671 idx := -1 672 for i, line := range hdrs { 673 if strings.TrimSpace(line) == "" { 674 idx = i 675 break 676 } 677 } 678 679 if idx > -1 { 680 title = hdrs[:idx] 681 if len(hdrs) > idx+1 { 682 desc = hdrs[idx+1:] 683 } else { 684 desc = nil 685 } 686 return 687 } 688 689 if len(hdrs) > 0 { 690 line := hdrs[0] 691 if rxPunctuationEnd.MatchString(line) { 692 title = []string{line} 693 desc = hdrs[1:] 694 } else { 695 desc = hdrs 696 } 697 } 698 699 return 700 } 701 702 func (sp *yamlSpecScanner) collectTitleDescription() { 703 if sp.workedOutTitle { 704 return 705 } 706 if sp.setTitle == nil { 707 sp.header = cleanupScannerLines(sp.header, rxUncommentHeaders, nil) 708 return 709 } 710 711 sp.workedOutTitle = true 712 sp.title, sp.header = collectScannerTitleDescription(sp.header) 713 } 714 715 func (sp *yamlSpecScanner) Title() []string { 716 sp.collectTitleDescription() 717 return sp.title 718 } 719 720 func (sp *yamlSpecScanner) Description() []string { 721 sp.collectTitleDescription() 722 return sp.header 723 } 724 725 func (sp *yamlSpecScanner) Parse(doc *ast.CommentGroup) error { 726 if doc == nil { 727 return nil 728 } 729 var startedYAMLSpec bool 730 COMMENTS: 731 for _, c := range doc.List { 732 for _, line := range strings.Split(c.Text, "\n") { 733 if rxSwaggerAnnotation.MatchString(line) { 734 break COMMENTS // a new swagger: annotation terminates this parser 735 } 736 737 if !startedYAMLSpec { 738 if rxBeginYAMLSpec.MatchString(line) { 739 startedYAMLSpec = true 740 sp.yamlSpec = append(sp.yamlSpec, line) 741 continue 742 } 743 744 if !sp.skipHeader { 745 sp.header = append(sp.header, line) 746 } 747 748 // no YAML spec yet, moving on 749 continue 750 } 751 752 sp.yamlSpec = append(sp.yamlSpec, line) 753 } 754 } 755 if sp.setTitle != nil { 756 sp.setTitle(sp.Title()) 757 } 758 if sp.setDescription != nil { 759 sp.setDescription(sp.Description()) 760 } 761 return nil 762 } 763 764 func (sp *yamlSpecScanner) UnmarshalSpec(u func([]byte) error) (err error) { 765 spec := cleanupScannerLines(sp.yamlSpec, rxUncommentYAML, nil) 766 if len(spec) == 0 { 767 return errors.New("no spec available to unmarshal") 768 } 769 770 if !strings.Contains(spec[0], "---") { 771 return errors.New("yaml spec has to start with `---`") 772 } 773 774 // remove indentation 775 spec = removeIndent(spec) 776 777 // 1. parse yaml lines 778 yamlValue := make(map[interface{}]interface{}) 779 780 yamlContent := strings.Join(spec, "\n") 781 err = yaml.Unmarshal([]byte(yamlContent), &yamlValue) 782 if err != nil { 783 return 784 } 785 786 // 2. convert to json 787 var jsonValue json.RawMessage 788 jsonValue, err = fmts.YAMLToJSON(yamlValue) 789 if err != nil { 790 return 791 } 792 793 // 3. unmarshal the json into an interface 794 var data []byte 795 data, err = jsonValue.MarshalJSON() 796 if err != nil { 797 return 798 } 799 err = u(data) 800 if err != nil { 801 return 802 } 803 804 // all parsed, returning... 805 sp.yamlSpec = nil // spec is now consumed, so let's erase the parsed lines 806 return 807 } 808 809 // removes indent base on the first line 810 func removeIndent(spec []string) []string { 811 loc := rxIndent.FindStringIndex(spec[0]) 812 if loc[1] > 0 { 813 for i := range spec { 814 if len(spec[i]) >= loc[1] { 815 spec[i] = spec[i][loc[1]-1:] 816 } 817 } 818 } 819 return spec 820 } 821 822 // removes indent base on the first line 823 func removeYamlIndent(spec []string) []string { 824 loc := rxIndent.FindStringIndex(spec[0]) 825 var s []string 826 if loc[1] > 0 { 827 for i := range spec { 828 if len(spec[i]) >= loc[1] { 829 s = append(s, spec[i][loc[1]-1:]) 830 } 831 } 832 } 833 return s 834 } 835 836 // aggregates lines in header until it sees a tag. 837 type sectionedParser struct { 838 header []string 839 matched map[string]tagParser 840 annotation valueParser 841 842 seenTag bool 843 skipHeader bool 844 setTitle func([]string) 845 setDescription func([]string) 846 workedOutTitle bool 847 taggers []tagParser 848 currentTagger *tagParser 849 title []string 850 ignored bool 851 } 852 853 func (st *sectionedParser) collectTitleDescription() { 854 if st.workedOutTitle { 855 return 856 } 857 if st.setTitle == nil { 858 st.header = cleanupScannerLines(st.header, rxUncommentHeaders, nil) 859 return 860 } 861 862 st.workedOutTitle = true 863 st.title, st.header = collectScannerTitleDescription(st.header) 864 } 865 866 func (st *sectionedParser) Title() []string { 867 st.collectTitleDescription() 868 return st.title 869 } 870 871 func (st *sectionedParser) Description() []string { 872 st.collectTitleDescription() 873 return st.header 874 } 875 876 func (st *sectionedParser) Parse(doc *ast.CommentGroup) error { 877 if doc == nil { 878 return nil 879 } 880 COMMENTS: 881 for _, c := range doc.List { 882 for _, line := range strings.Split(c.Text, "\n") { 883 if rxSwaggerAnnotation.MatchString(line) { 884 if rxIgnoreOverride.MatchString(line) { 885 st.ignored = true 886 break COMMENTS // an explicit ignore terminates this parser 887 } 888 if st.annotation == nil || !st.annotation.Matches(line) { 889 break COMMENTS // a new swagger: annotation terminates this parser 890 } 891 892 _ = st.annotation.Parse([]string{line}) 893 if len(st.header) > 0 { 894 st.seenTag = true 895 } 896 continue 897 } 898 899 var matched bool 900 for _, tagger := range st.taggers { 901 if tagger.Matches(line) { 902 st.seenTag = true 903 st.currentTagger = &tagger 904 matched = true 905 break 906 } 907 } 908 909 if st.currentTagger == nil { 910 if !st.skipHeader && !st.seenTag { 911 st.header = append(st.header, line) 912 } 913 // didn't match a tag, moving on 914 continue 915 } 916 917 if st.currentTagger.MultiLine && matched { 918 // the first line of a multiline tagger doesn't count 919 continue 920 } 921 922 ts, ok := st.matched[st.currentTagger.Name] 923 if !ok { 924 ts = *st.currentTagger 925 } 926 ts.Lines = append(ts.Lines, line) 927 if st.matched == nil { 928 st.matched = make(map[string]tagParser) 929 } 930 st.matched[st.currentTagger.Name] = ts 931 932 if !st.currentTagger.MultiLine { 933 st.currentTagger = nil 934 } 935 } 936 } 937 if st.setTitle != nil { 938 st.setTitle(st.Title()) 939 } 940 if st.setDescription != nil { 941 st.setDescription(st.Description()) 942 } 943 for _, mt := range st.matched { 944 if !mt.SkipCleanUp { 945 mt.Lines = cleanupScannerLines(mt.Lines, rxUncommentHeaders, nil) 946 } 947 if err := mt.Parse(mt.Lines); err != nil { 948 return err 949 } 950 } 951 return nil 952 }