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