github.com/emreu/go-swagger@v0.22.1/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 Schema() *spec.Schema 475 Level() int 476 } 477 478 // Map all Go builtin types that have Json representation to Swagger/Json types. 479 // See https://golang.org/pkg/builtin/ and http://swagger.io/specification/ 480 func swaggerSchemaForType(typeName string, prop swaggerTypable) error { 481 switch typeName { 482 case "bool": 483 prop.Typed("boolean", "") 484 case "byte": 485 prop.Typed("integer", "uint8") 486 case "complex128", "complex64": 487 return fmt.Errorf("unsupported builtin %q (no JSON marshaller)", typeName) 488 case "error": 489 // TODO: error is often marshalled into a string but not always (e.g. errors package creates 490 // errors that are marshalled into an empty object), this could be handled the same way 491 // custom JSON marshallers are handled (in future) 492 prop.Typed("string", "") 493 case "float32": 494 prop.Typed("number", "float") 495 case "float64": 496 prop.Typed("number", "double") 497 case "int": 498 prop.Typed("integer", "int64") 499 case "int16": 500 prop.Typed("integer", "int16") 501 case "int32": 502 prop.Typed("integer", "int32") 503 case "int64": 504 prop.Typed("integer", "int64") 505 case "int8": 506 prop.Typed("integer", "int8") 507 case "rune": 508 prop.Typed("integer", "int32") 509 case "string": 510 prop.Typed("string", "") 511 case "uint": 512 prop.Typed("integer", "uint64") 513 case "uint16": 514 prop.Typed("integer", "uint16") 515 case "uint32": 516 prop.Typed("integer", "uint32") 517 case "uint64": 518 prop.Typed("integer", "uint64") 519 case "uint8": 520 prop.Typed("integer", "uint8") 521 case "uintptr": 522 prop.Typed("integer", "uint64") 523 default: 524 return fmt.Errorf("unsupported type %q", typeName) 525 } 526 return nil 527 } 528 529 func newMultiLineTagParser(name string, parser valueParser, skipCleanUp bool) tagParser { 530 return tagParser{ 531 Name: name, 532 MultiLine: true, 533 SkipCleanUp: skipCleanUp, 534 Parser: parser, 535 } 536 } 537 538 func newSingleLineTagParser(name string, parser valueParser) tagParser { 539 return tagParser{ 540 Name: name, 541 MultiLine: false, 542 SkipCleanUp: false, 543 Parser: parser, 544 } 545 } 546 547 type tagParser struct { 548 Name string 549 MultiLine bool 550 SkipCleanUp bool 551 Lines []string 552 Parser valueParser 553 } 554 555 func (st *tagParser) Matches(line string) bool { 556 return st.Parser.Matches(line) 557 } 558 559 func (st *tagParser) Parse(lines []string) error { 560 return st.Parser.Parse(lines) 561 } 562 563 func newYamlParser(rx *regexp.Regexp, setter func(json.RawMessage) error) valueParser { 564 return &yamlParser{ 565 set: setter, 566 rx: rx, 567 } 568 } 569 570 type yamlParser struct { 571 set func(json.RawMessage) error 572 rx *regexp.Regexp 573 } 574 575 func (y *yamlParser) Parse(lines []string) error { 576 if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) { 577 return nil 578 } 579 580 var uncommented []string 581 uncommented = append(uncommented, removeYamlIndent(lines)...) 582 583 yamlContent := strings.Join(uncommented, "\n") 584 var yamlValue interface{} 585 err := yaml.Unmarshal([]byte(yamlContent), &yamlValue) 586 if err != nil { 587 return err 588 } 589 590 var jsonValue json.RawMessage 591 jsonValue, err = fmts.YAMLToJSON(yamlValue) 592 if err != nil { 593 return err 594 } 595 596 return y.set(jsonValue) 597 } 598 599 func (y *yamlParser) Matches(line string) bool { 600 return y.rx.MatchString(line) 601 } 602 603 // aggregates lines in header until it sees `---`, 604 // the beginning of a YAML spec 605 type yamlSpecScanner struct { 606 header []string 607 yamlSpec []string 608 setTitle func([]string) 609 setDescription func([]string) 610 workedOutTitle bool 611 title []string 612 skipHeader bool 613 } 614 615 func cleanupScannerLines(lines []string, ur *regexp.Regexp, yamlBlock *regexp.Regexp) []string { 616 // bail early when there is nothing to parse 617 if len(lines) == 0 { 618 return lines 619 } 620 seenLine := -1 621 var lastContent int 622 var uncommented []string 623 var startBlock bool 624 var yaml []string 625 for i, v := range lines { 626 if yamlBlock != nil && yamlBlock.MatchString(v) && !startBlock { 627 startBlock = true 628 if seenLine < 0 { 629 seenLine = i 630 } 631 continue 632 } 633 if startBlock { 634 if yamlBlock.MatchString(v) { 635 startBlock = false 636 uncommented = append(uncommented, removeIndent(yaml)...) 637 continue 638 } 639 yaml = append(yaml, v) 640 if v != "" { 641 if seenLine < 0 { 642 seenLine = i 643 } 644 lastContent = i 645 } 646 continue 647 } 648 str := ur.ReplaceAllString(v, "") 649 uncommented = append(uncommented, str) 650 if str != "" { 651 if seenLine < 0 { 652 seenLine = i 653 } 654 lastContent = i 655 } 656 } 657 658 // fixes issue #50 659 if seenLine == -1 { 660 return nil 661 } 662 return uncommented[seenLine : lastContent+1] 663 } 664 665 // a shared function that can be used to split given headers 666 // into a title and description 667 func collectScannerTitleDescription(headers []string) (title, desc []string) { 668 hdrs := cleanupScannerLines(headers, rxUncommentHeaders, nil) 669 670 idx := -1 671 for i, line := range hdrs { 672 if strings.TrimSpace(line) == "" { 673 idx = i 674 break 675 } 676 } 677 678 if idx > -1 { 679 title = hdrs[:idx] 680 if len(hdrs) > idx+1 { 681 desc = hdrs[idx+1:] 682 } else { 683 desc = nil 684 } 685 return 686 } 687 688 if len(hdrs) > 0 { 689 line := hdrs[0] 690 if rxPunctuationEnd.MatchString(line) { 691 title = []string{line} 692 desc = hdrs[1:] 693 } else { 694 desc = hdrs 695 } 696 } 697 698 return 699 } 700 701 func (sp *yamlSpecScanner) collectTitleDescription() { 702 if sp.workedOutTitle { 703 return 704 } 705 if sp.setTitle == nil { 706 sp.header = cleanupScannerLines(sp.header, rxUncommentHeaders, nil) 707 return 708 } 709 710 sp.workedOutTitle = true 711 sp.title, sp.header = collectScannerTitleDescription(sp.header) 712 } 713 714 func (sp *yamlSpecScanner) Title() []string { 715 sp.collectTitleDescription() 716 return sp.title 717 } 718 719 func (sp *yamlSpecScanner) Description() []string { 720 sp.collectTitleDescription() 721 return sp.header 722 } 723 724 func (sp *yamlSpecScanner) Parse(doc *ast.CommentGroup) error { 725 if doc == nil { 726 return nil 727 } 728 var startedYAMLSpec bool 729 COMMENTS: 730 for _, c := range doc.List { 731 for _, line := range strings.Split(c.Text, "\n") { 732 if rxSwaggerAnnotation.MatchString(line) { 733 break COMMENTS // a new swagger: annotation terminates this parser 734 } 735 736 if !startedYAMLSpec { 737 if rxBeginYAMLSpec.MatchString(line) { 738 startedYAMLSpec = true 739 sp.yamlSpec = append(sp.yamlSpec, line) 740 continue 741 } 742 743 if !sp.skipHeader { 744 sp.header = append(sp.header, line) 745 } 746 747 // no YAML spec yet, moving on 748 continue 749 } 750 751 sp.yamlSpec = append(sp.yamlSpec, line) 752 } 753 } 754 if sp.setTitle != nil { 755 sp.setTitle(sp.Title()) 756 } 757 if sp.setDescription != nil { 758 sp.setDescription(sp.Description()) 759 } 760 return nil 761 } 762 763 func (sp *yamlSpecScanner) UnmarshalSpec(u func([]byte) error) (err error) { 764 spec := cleanupScannerLines(sp.yamlSpec, rxUncommentYAML, nil) 765 if len(spec) == 0 { 766 return errors.New("no spec available to unmarshal") 767 } 768 769 if !strings.Contains(spec[0], "---") { 770 return errors.New("yaml spec has to start with `---`") 771 } 772 773 // remove indentation 774 spec = removeIndent(spec) 775 776 // 1. parse yaml lines 777 yamlValue := make(map[interface{}]interface{}) 778 779 yamlContent := strings.Join(spec, "\n") 780 err = yaml.Unmarshal([]byte(yamlContent), &yamlValue) 781 if err != nil { 782 return 783 } 784 785 // 2. convert to json 786 var jsonValue json.RawMessage 787 jsonValue, err = fmts.YAMLToJSON(yamlValue) 788 if err != nil { 789 return 790 } 791 792 // 3. unmarshal the json into an interface 793 var data []byte 794 data, err = jsonValue.MarshalJSON() 795 if err != nil { 796 return 797 } 798 err = u(data) 799 if err != nil { 800 return 801 } 802 803 // all parsed, returning... 804 sp.yamlSpec = nil // spec is now consumed, so let's erase the parsed lines 805 return 806 } 807 808 // removes indent base on the first line 809 func removeIndent(spec []string) []string { 810 loc := rxIndent.FindStringIndex(spec[0]) 811 if loc[1] > 0 { 812 for i := range spec { 813 if len(spec[i]) >= loc[1] { 814 spec[i] = spec[i][loc[1]-1:] 815 } 816 } 817 } 818 return spec 819 } 820 821 // removes indent base on the first line 822 func removeYamlIndent(spec []string) []string { 823 loc := rxIndent.FindStringIndex(spec[0]) 824 var s []string 825 if loc[1] > 0 { 826 for i := range spec { 827 if len(spec[i]) >= loc[1] { 828 s = append(s, spec[i][loc[1]-1:]) 829 } 830 } 831 } 832 return s 833 } 834 835 // aggregates lines in header until it sees a tag. 836 type sectionedParser struct { 837 header []string 838 matched map[string]tagParser 839 annotation valueParser 840 841 seenTag bool 842 skipHeader bool 843 setTitle func([]string) 844 setDescription func([]string) 845 workedOutTitle bool 846 taggers []tagParser 847 currentTagger *tagParser 848 title []string 849 ignored bool 850 } 851 852 func (st *sectionedParser) collectTitleDescription() { 853 if st.workedOutTitle { 854 return 855 } 856 if st.setTitle == nil { 857 st.header = cleanupScannerLines(st.header, rxUncommentHeaders, nil) 858 return 859 } 860 861 st.workedOutTitle = true 862 st.title, st.header = collectScannerTitleDescription(st.header) 863 } 864 865 func (st *sectionedParser) Title() []string { 866 st.collectTitleDescription() 867 return st.title 868 } 869 870 func (st *sectionedParser) Description() []string { 871 st.collectTitleDescription() 872 return st.header 873 } 874 875 func (st *sectionedParser) Parse(doc *ast.CommentGroup) error { 876 if doc == nil { 877 return nil 878 } 879 COMMENTS: 880 for _, c := range doc.List { 881 for _, line := range strings.Split(c.Text, "\n") { 882 if rxSwaggerAnnotation.MatchString(line) { 883 if rxIgnoreOverride.MatchString(line) { 884 st.ignored = true 885 break COMMENTS // an explicit ignore terminates this parser 886 } 887 if st.annotation == nil || !st.annotation.Matches(line) { 888 break COMMENTS // a new swagger: annotation terminates this parser 889 } 890 891 _ = st.annotation.Parse([]string{line}) 892 if len(st.header) > 0 { 893 st.seenTag = true 894 } 895 continue 896 } 897 898 var matched bool 899 for _, tagger := range st.taggers { 900 if tagger.Matches(line) { 901 st.seenTag = true 902 st.currentTagger = &tagger 903 matched = true 904 break 905 } 906 } 907 908 if st.currentTagger == nil { 909 if !st.skipHeader && !st.seenTag { 910 st.header = append(st.header, line) 911 } 912 // didn't match a tag, moving on 913 continue 914 } 915 916 if st.currentTagger.MultiLine && matched { 917 // the first line of a multiline tagger doesn't count 918 continue 919 } 920 921 ts, ok := st.matched[st.currentTagger.Name] 922 if !ok { 923 ts = *st.currentTagger 924 } 925 ts.Lines = append(ts.Lines, line) 926 if st.matched == nil { 927 st.matched = make(map[string]tagParser) 928 } 929 st.matched[st.currentTagger.Name] = ts 930 931 if !st.currentTagger.MultiLine { 932 st.currentTagger = nil 933 } 934 } 935 } 936 if st.setTitle != nil { 937 st.setTitle(st.Title()) 938 } 939 if st.setDescription != nil { 940 st.setDescription(st.Description()) 941 } 942 for _, mt := range st.matched { 943 if !mt.SkipCleanUp { 944 mt.Lines = cleanupScannerLines(mt.Lines, rxUncommentHeaders, nil) 945 } 946 if err := mt.Parse(mt.Lines); err != nil { 947 return err 948 } 949 } 950 return nil 951 }