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