|
- // +build !go1.11
-
- // Copyright 2015 go-swagger maintainers
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
-
- package scan
-
- import (
- "encoding/json"
- "errors"
- "fmt"
- "go/ast"
- "go/build"
- goparser "go/parser"
- "go/types"
- "log"
- "os"
- "regexp"
- "strings"
-
- "github.com/go-openapi/loads/fmts"
- "github.com/go-openapi/spec"
- "github.com/go-openapi/swag"
- "golang.org/x/tools/go/loader"
- yaml "gopkg.in/yaml.v2"
- )
-
- const (
- rxMethod = "(\\p{L}+)"
- rxPath = "((?:/[\\p{L}\\p{N}\\p{Pd}\\p{Pc}{}\\-\\.\\?_~%!$&'()*+,;=:@/]*)+/?)"
- rxOpTags = "(\\p{L}[\\p{L}\\p{N}\\p{Pd}\\.\\p{Pc}\\p{Zs}]+)"
- rxOpID = "((?:\\p{L}[\\p{L}\\p{N}\\p{Pd}\\p{Pc}]+)+)"
-
- rxMaximumFmt = "%s[Mm]ax(?:imum)?\\p{Zs}*:\\p{Zs}*([\\<=])?\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$"
- rxMinimumFmt = "%s[Mm]in(?:imum)?\\p{Zs}*:\\p{Zs}*([\\>=])?\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$"
- rxMultipleOfFmt = "%s[Mm]ultiple\\p{Zs}*[Oo]f\\p{Zs}*:\\p{Zs}*([\\+-]?(?:\\p{N}+\\.)?\\p{N}+)$"
-
- rxMaxLengthFmt = "%s[Mm]ax(?:imum)?(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ll]en(?:gth)?)\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
- rxMinLengthFmt = "%s[Mm]in(?:imum)?(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ll]en(?:gth)?)\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
- rxPatternFmt = "%s[Pp]attern\\p{Zs}*:\\p{Zs}*(.*)$"
- rxCollectionFormatFmt = "%s[Cc]ollection(?:\\p{Zs}*[\\p{Pd}\\p{Pc}]?[Ff]ormat)\\p{Zs}*:\\p{Zs}*(.*)$"
- rxEnumFmt = "%s[Ee]num\\p{Zs}*:\\p{Zs}*(.*)$"
- rxDefaultFmt = "%s[Dd]efault\\p{Zs}*:\\p{Zs}*(.*)$"
- rxExampleFmt = "%s[Ee]xample\\p{Zs}*:\\p{Zs}*(.*)$"
-
- rxMaxItemsFmt = "%s[Mm]ax(?:imum)?(?:\\p{Zs}*|[\\p{Pd}\\p{Pc}]|\\.)?[Ii]tems\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
- rxMinItemsFmt = "%s[Mm]in(?:imum)?(?:\\p{Zs}*|[\\p{Pd}\\p{Pc}]|\\.)?[Ii]tems\\p{Zs}*:\\p{Zs}*(\\p{N}+)$"
- rxUniqueFmt = "%s[Uu]nique\\p{Zs}*:\\p{Zs}*(true|false)$"
-
- rxItemsPrefixFmt = "(?:[Ii]tems[\\.\\p{Zs}]*){%d}"
- )
-
- var (
- rxSwaggerAnnotation = regexp.MustCompile(`swagger:([\p{L}\p{N}\p{Pd}\p{Pc}]+)`)
- rxFileUpload = regexp.MustCompile(`swagger:file`)
- rxStrFmt = regexp.MustCompile(`swagger:strfmt\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
- rxAlias = regexp.MustCompile(`swagger:alias`)
- rxName = regexp.MustCompile(`swagger:name\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\.]+)$`)
- rxAllOf = regexp.MustCompile(`swagger:allOf\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\.]+)?$`)
- rxModelOverride = regexp.MustCompile(`swagger:model\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`)
- rxResponseOverride = regexp.MustCompile(`swagger:response\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`)
- rxParametersOverride = regexp.MustCompile(`swagger:parameters\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}\p{Zs}]+)$`)
- rxEnum = regexp.MustCompile(`swagger:enum\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
- rxIgnoreOverride = regexp.MustCompile(`swagger:ignore\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)?$`)
- rxDefault = regexp.MustCompile(`swagger:default\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
- rxType = regexp.MustCompile(`swagger:type\p{Zs}*(\p{L}[\p{L}\p{N}\p{Pd}\p{Pc}]+)$`)
- rxRoute = regexp.MustCompile(
- "swagger:route\\p{Zs}*" +
- rxMethod +
- "\\p{Zs}*" +
- rxPath +
- "(?:\\p{Zs}+" +
- rxOpTags +
- ")?\\p{Zs}+" +
- rxOpID + "\\p{Zs}*$")
- rxBeginYAMLSpec = regexp.MustCompile(`---\p{Zs}*$`)
- rxUncommentHeaders = regexp.MustCompile(`^[\p{Zs}\t/\*-]*\|?`)
- rxUncommentYAML = regexp.MustCompile(`^[\p{Zs}\t]*/*`)
- rxOperation = regexp.MustCompile(
- "swagger:operation\\p{Zs}*" +
- rxMethod +
- "\\p{Zs}*" +
- rxPath +
- "(?:\\p{Zs}+" +
- rxOpTags +
- ")?\\p{Zs}+" +
- rxOpID + "\\p{Zs}*$")
-
- rxSpace = regexp.MustCompile(`\p{Zs}+`)
- rxIndent = regexp.MustCompile(`\p{Zs}*/*\p{Zs}*[^\p{Zs}]`)
- rxPunctuationEnd = regexp.MustCompile(`\p{Po}$`)
- rxStripComments = regexp.MustCompile(`^[^\p{L}\p{N}\p{Pd}\p{Pc}\+]*`)
- rxStripTitleComments = regexp.MustCompile(`^[^\p{L}]*[Pp]ackage\p{Zs}+[^\p{Zs}]+\p{Zs}*`)
- rxAllowedExtensions = regexp.MustCompile(`^[Xx]-`)
-
- rxIn = regexp.MustCompile(`[Ii]n\p{Zs}*:\p{Zs}*(query|path|header|body|formData)$`)
- rxRequired = regexp.MustCompile(`[Rr]equired\p{Zs}*:\p{Zs}*(true|false)$`)
- rxDiscriminator = regexp.MustCompile(`[Dd]iscriminator\p{Zs}*:\p{Zs}*(true|false)$`)
- rxReadOnly = regexp.MustCompile(`[Rr]ead(?:\p{Zs}*|[\p{Pd}\p{Pc}])?[Oo]nly\p{Zs}*:\p{Zs}*(true|false)$`)
- rxConsumes = regexp.MustCompile(`[Cc]onsumes\p{Zs}*:`)
- rxProduces = regexp.MustCompile(`[Pp]roduces\p{Zs}*:`)
- rxSecuritySchemes = regexp.MustCompile(`[Ss]ecurity\p{Zs}*:`)
- rxSecurity = regexp.MustCompile(`[Ss]ecurity\p{Zs}*[Dd]efinitions:`)
- rxResponses = regexp.MustCompile(`[Rr]esponses\p{Zs}*:`)
- rxParameters = regexp.MustCompile(`[Pp]arameters\p{Zs}*:`)
- rxSchemes = regexp.MustCompile(`[Ss]chemes\p{Zs}*:\p{Zs}*((?:(?:https?|HTTPS?|wss?|WSS?)[\p{Zs},]*)+)$`)
- rxVersion = regexp.MustCompile(`[Vv]ersion\p{Zs}*:\p{Zs}*(.+)$`)
- rxHost = regexp.MustCompile(`[Hh]ost\p{Zs}*:\p{Zs}*(.+)$`)
- rxBasePath = regexp.MustCompile(`[Bb]ase\p{Zs}*-*[Pp]ath\p{Zs}*:\p{Zs}*` + rxPath + "$")
- rxLicense = regexp.MustCompile(`[Ll]icense\p{Zs}*:\p{Zs}*(.+)$`)
- rxContact = regexp.MustCompile(`[Cc]ontact\p{Zs}*-?(?:[Ii]info\p{Zs}*)?:\p{Zs}*(.+)$`)
- rxTOS = regexp.MustCompile(`[Tt](:?erms)?\p{Zs}*-?[Oo]f?\p{Zs}*-?[Ss](?:ervice)?\p{Zs}*:`)
- rxExtensions = regexp.MustCompile(`[Ee]xtensions\p{Zs}*:`)
- rxInfoExtensions = regexp.MustCompile(`[In]nfo\p{Zs}*[Ee]xtensions:`)
- // currently unused: rxExample = regexp.MustCompile(`[Ex]ample\p{Zs}*:\p{Zs}*(.*)$`)
- )
-
- // Many thanks go to https://github.com/yvasiyarov/swagger
- // this is loosely based on that implementation but for swagger 2.0
-
- func joinDropLast(lines []string) string {
- l := len(lines)
- lns := lines
- if l > 0 && len(strings.TrimSpace(lines[l-1])) == 0 {
- lns = lines[:l-1]
- }
- return strings.Join(lns, "\n")
- }
-
- func removeEmptyLines(lines []string) (notEmpty []string) {
- for _, l := range lines {
- if len(strings.TrimSpace(l)) > 0 {
- notEmpty = append(notEmpty, l)
- }
- }
- return
- }
-
- func rxf(rxp, ar string) *regexp.Regexp {
- return regexp.MustCompile(fmt.Sprintf(rxp, ar))
- }
-
- // The Opts for the application scanner.
- type Opts struct {
- BasePath string
- Input *spec.Swagger
- ScanModels bool
- BuildTags string
- Include []string
- Exclude []string
- IncludeTags []string
- ExcludeTags []string
- }
-
- func safeConvert(str string) bool {
- b, err := swag.ConvertBool(str)
- if err != nil {
- return false
- }
- return b
- }
-
- // Debug is true when process is run with DEBUG=1 env var
- var Debug = safeConvert(os.Getenv("DEBUG"))
-
- // Application scans the application and builds a swagger spec based on the information from the code files.
- // When there are includes provided, only those files are considered for the initial discovery.
- // Similarly the excludes will exclude an item from initial discovery through scanning for annotations.
- // When something in the discovered items requires a type that is contained in the includes or excludes it will still be
- // in the spec.
- func Application(opts Opts) (*spec.Swagger, error) {
- parser, err := newAppScanner(&opts)
-
- if err != nil {
- return nil, err
- }
- return parser.Parse()
- }
-
- // appScanner the global context for scanning a go application
- // into a swagger specification
- type appScanner struct {
- loader *loader.Config
- prog *loader.Program
- classifier *programClassifier
- discovered []schemaDecl
- input *spec.Swagger
- definitions map[string]spec.Schema
- responses map[string]spec.Response
- operations map[string]*spec.Operation
- scanModels bool
- includeTags map[string]bool
- excludeTas map[string]bool
-
- // MainPackage the path to find the main class in
- MainPackage string
- }
-
- // newAppScanner creates a new api parser
- func newAppScanner(opts *Opts) (*appScanner, error) {
- if Debug {
- log.Println("scanning packages discovered through entrypoint @ ", opts.BasePath)
- }
- var ldr loader.Config
- ldr.ParserMode = goparser.ParseComments
- ldr.Import(opts.BasePath)
- if opts.BuildTags != "" {
- ldr.Build = &build.Default
- ldr.Build.BuildTags = strings.Split(opts.BuildTags, ",")
- }
- ldr.TypeChecker = types.Config{FakeImportC: true}
- prog, err := ldr.Load()
- if err != nil {
- return nil, err
- }
-
- var includes, excludes packageFilters
- if len(opts.Include) > 0 {
- for _, include := range opts.Include {
- includes = append(includes, packageFilter{Name: include})
- }
- }
- if len(opts.Exclude) > 0 {
- for _, exclude := range opts.Exclude {
- excludes = append(excludes, packageFilter{Name: exclude})
- }
- }
- includeTags := make(map[string]bool)
- for _, includeTag := range opts.IncludeTags {
- includeTags[includeTag] = true
- }
- excludeTags := make(map[string]bool)
- for _, excludeTag := range opts.ExcludeTags {
- excludeTags[excludeTag] = true
- }
-
- input := opts.Input
- if input == nil {
- input = new(spec.Swagger)
- input.Swagger = "2.0"
- }
-
- if input.Paths == nil {
- input.Paths = new(spec.Paths)
- }
- if input.Definitions == nil {
- input.Definitions = make(map[string]spec.Schema)
- }
- if input.Responses == nil {
- input.Responses = make(map[string]spec.Response)
- }
- if input.Extensions == nil {
- input.Extensions = make(spec.Extensions)
- }
-
- return &appScanner{
- MainPackage: opts.BasePath,
- prog: prog,
- input: input,
- loader: &ldr,
- operations: collectOperationsFromInput(input),
- definitions: input.Definitions,
- responses: input.Responses,
- scanModels: opts.ScanModels,
- classifier: &programClassifier{
- Includes: includes,
- Excludes: excludes,
- },
- includeTags: includeTags,
- excludeTas: excludeTags,
- }, nil
- }
-
- func collectOperationsFromInput(input *spec.Swagger) map[string]*spec.Operation {
- operations := make(map[string]*spec.Operation)
- if input != nil && input.Paths != nil {
- for _, pth := range input.Paths.Paths {
- if pth.Get != nil {
- operations[pth.Get.ID] = pth.Get
- }
- if pth.Post != nil {
- operations[pth.Post.ID] = pth.Post
- }
- if pth.Put != nil {
- operations[pth.Put.ID] = pth.Put
- }
- if pth.Patch != nil {
- operations[pth.Patch.ID] = pth.Patch
- }
- if pth.Delete != nil {
- operations[pth.Delete.ID] = pth.Delete
- }
- if pth.Head != nil {
- operations[pth.Head.ID] = pth.Head
- }
- if pth.Options != nil {
- operations[pth.Options.ID] = pth.Options
- }
- }
- }
- return operations
- }
-
- // Parse produces a swagger object for an application
- func (a *appScanner) Parse() (*spec.Swagger, error) {
- // classification still includes files that are completely commented out
- cp, err := a.classifier.Classify(a.prog)
- if err != nil {
- return nil, err
- }
-
- // build models dictionary
- if a.scanModels {
- for _, modelsFile := range cp.Models {
- if err := a.parseSchema(modelsFile); err != nil {
- return nil, err
- }
- }
- }
-
- // build parameters dictionary
- for _, paramsFile := range cp.Parameters {
- if err := a.parseParameters(paramsFile); err != nil {
- return nil, err
- }
- }
-
- // build responses dictionary
- for _, responseFile := range cp.Responses {
- if err := a.parseResponses(responseFile); err != nil {
- return nil, err
- }
- }
-
- // build definitions dictionary
- if err := a.processDiscovered(); err != nil {
- return nil, err
- }
-
- // build paths dictionary
- for _, routeFile := range cp.Routes {
- if err := a.parseRoutes(routeFile); err != nil {
- return nil, err
- }
- }
- for _, operationFile := range cp.Operations {
- if err := a.parseOperations(operationFile); err != nil {
- return nil, err
- }
- }
-
- // build swagger object
- for _, metaFile := range cp.Meta {
- if err := a.parseMeta(metaFile); err != nil {
- return nil, err
- }
- }
-
- if a.input.Swagger == "" {
- a.input.Swagger = "2.0"
- }
-
- return a.input, nil
- }
-
- func (a *appScanner) processDiscovered() error {
- // loop over discovered until all the items are in definitions
- keepGoing := len(a.discovered) > 0
- for keepGoing {
- var queue []schemaDecl
- for _, d := range a.discovered {
- if _, ok := a.definitions[d.Name]; !ok {
- queue = append(queue, d)
- }
- }
- a.discovered = nil
- for _, sd := range queue {
- if err := a.parseDiscoveredSchema(sd); err != nil {
- return err
- }
- }
- keepGoing = len(a.discovered) > 0
- }
-
- return nil
- }
-
- func (a *appScanner) parseSchema(file *ast.File) error {
- sp := newSchemaParser(a.prog)
- if err := sp.Parse(file, a.definitions); err != nil {
- return err
- }
- a.discovered = append(a.discovered, sp.postDecls...)
- return nil
- }
-
- func (a *appScanner) parseDiscoveredSchema(sd schemaDecl) error {
- sp := newSchemaParser(a.prog)
- sp.discovered = &sd
-
- if err := sp.Parse(sd.File, a.definitions); err != nil {
- return err
- }
- a.discovered = append(a.discovered, sp.postDecls...)
- return nil
- }
-
- func (a *appScanner) parseRoutes(file *ast.File) error {
- rp := newRoutesParser(a.prog)
- rp.operations = a.operations
- rp.definitions = a.definitions
- rp.responses = a.responses
-
- return rp.Parse(file, a.input.Paths, a.includeTags, a.excludeTas)
- }
-
- func (a *appScanner) parseOperations(file *ast.File) error {
- op := newOperationsParser(a.prog)
- op.operations = a.operations
- op.definitions = a.definitions
- op.responses = a.responses
- return op.Parse(file, a.input.Paths, a.includeTags, a.excludeTas)
- }
-
- func (a *appScanner) parseParameters(file *ast.File) error {
- rp := newParameterParser(a.prog)
- if err := rp.Parse(file, a.operations); err != nil {
- return err
- }
- a.discovered = append(a.discovered, rp.postDecls...)
- a.discovered = append(a.discovered, rp.scp.postDecls...)
- return nil
- }
-
- func (a *appScanner) parseResponses(file *ast.File) error {
- rp := newResponseParser(a.prog)
- if err := rp.Parse(file, a.responses); err != nil {
- return err
- }
- a.discovered = append(a.discovered, rp.postDecls...)
- a.discovered = append(a.discovered, rp.scp.postDecls...)
- return nil
- }
-
- func (a *appScanner) parseMeta(file *ast.File) error {
- return newMetaParser(a.input).Parse(file.Doc)
- }
-
- // MustExpandPackagePath gets the real package path on disk
- func (a *appScanner) MustExpandPackagePath(packagePath string) string {
- pkgRealpath := swag.FindInGoSearchPath(packagePath)
- if pkgRealpath == "" {
- log.Fatalf("Can't find package %s \n", packagePath)
- }
-
- return pkgRealpath
- }
-
- type swaggerTypable interface {
- Typed(string, string)
- SetRef(spec.Ref)
- Items() swaggerTypable
- Schema() *spec.Schema
- Level() int
- }
-
- // Map all Go builtin types that have Json representation to Swagger/Json types.
- // See https://golang.org/pkg/builtin/ and http://swagger.io/specification/
- func swaggerSchemaForType(typeName string, prop swaggerTypable) error {
- switch typeName {
- case "bool":
- prop.Typed("boolean", "")
- case "byte":
- prop.Typed("integer", "uint8")
- case "complex128", "complex64":
- return fmt.Errorf("unsupported builtin %q (no JSON marshaller)", typeName)
- case "error":
- // TODO: error is often marshalled into a string but not always (e.g. errors package creates
- // errors that are marshalled into an empty object), this could be handled the same way
- // custom JSON marshallers are handled (in future)
- prop.Typed("string", "")
- case "float32":
- prop.Typed("number", "float")
- case "float64":
- prop.Typed("number", "double")
- case "int":
- prop.Typed("integer", "int64")
- case "int16":
- prop.Typed("integer", "int16")
- case "int32":
- prop.Typed("integer", "int32")
- case "int64":
- prop.Typed("integer", "int64")
- case "int8":
- prop.Typed("integer", "int8")
- case "rune":
- prop.Typed("integer", "int32")
- case "string":
- prop.Typed("string", "")
- case "uint":
- prop.Typed("integer", "uint64")
- case "uint16":
- prop.Typed("integer", "uint16")
- case "uint32":
- prop.Typed("integer", "uint32")
- case "uint64":
- prop.Typed("integer", "uint64")
- case "uint8":
- prop.Typed("integer", "uint8")
- case "uintptr":
- prop.Typed("integer", "uint64")
- default:
- return fmt.Errorf("unsupported type %q", typeName)
- }
- return nil
- }
-
- func newMultiLineTagParser(name string, parser valueParser, skipCleanUp bool) tagParser {
- return tagParser{
- Name: name,
- MultiLine: true,
- SkipCleanUp: skipCleanUp,
- Parser: parser,
- }
- }
-
- func newSingleLineTagParser(name string, parser valueParser) tagParser {
- return tagParser{
- Name: name,
- MultiLine: false,
- SkipCleanUp: false,
- Parser: parser,
- }
- }
-
- type tagParser struct {
- Name string
- MultiLine bool
- SkipCleanUp bool
- Lines []string
- Parser valueParser
- }
-
- func (st *tagParser) Matches(line string) bool {
- return st.Parser.Matches(line)
- }
-
- func (st *tagParser) Parse(lines []string) error {
- return st.Parser.Parse(lines)
- }
-
- func newYamlParser(rx *regexp.Regexp, setter func(json.RawMessage) error) valueParser {
- return &yamlParser{
- set: setter,
- rx: rx,
- }
- }
-
- type yamlParser struct {
- set func(json.RawMessage) error
- rx *regexp.Regexp
- }
-
- func (y *yamlParser) Parse(lines []string) error {
- if len(lines) == 0 || (len(lines) == 1 && len(lines[0]) == 0) {
- return nil
- }
-
- var uncommented []string
- uncommented = append(uncommented, removeYamlIndent(lines)...)
-
- yamlContent := strings.Join(uncommented, "\n")
- var yamlValue interface{}
- err := yaml.Unmarshal([]byte(yamlContent), &yamlValue)
- if err != nil {
- return err
- }
-
- var jsonValue json.RawMessage
- jsonValue, err = fmts.YAMLToJSON(yamlValue)
- if err != nil {
- return err
- }
-
- return y.set(jsonValue)
- }
-
- func (y *yamlParser) Matches(line string) bool {
- return y.rx.MatchString(line)
- }
-
- // aggregates lines in header until it sees `---`,
- // the beginning of a YAML spec
- type yamlSpecScanner struct {
- header []string
- yamlSpec []string
- setTitle func([]string)
- setDescription func([]string)
- workedOutTitle bool
- title []string
- skipHeader bool
- }
-
- func cleanupScannerLines(lines []string, ur *regexp.Regexp, yamlBlock *regexp.Regexp) []string {
- // bail early when there is nothing to parse
- if len(lines) == 0 {
- return lines
- }
- seenLine := -1
- var lastContent int
- var uncommented []string
- var startBlock bool
- var yaml []string
- for i, v := range lines {
- if yamlBlock != nil && yamlBlock.MatchString(v) && !startBlock {
- startBlock = true
- if seenLine < 0 {
- seenLine = i
- }
- continue
- }
- if startBlock {
- if yamlBlock.MatchString(v) {
- startBlock = false
- uncommented = append(uncommented, removeIndent(yaml)...)
- continue
- }
- yaml = append(yaml, v)
- if v != "" {
- if seenLine < 0 {
- seenLine = i
- }
- lastContent = i
- }
- continue
- }
- str := ur.ReplaceAllString(v, "")
- uncommented = append(uncommented, str)
- if str != "" {
- if seenLine < 0 {
- seenLine = i
- }
- lastContent = i
- }
- }
-
- // fixes issue #50
- if seenLine == -1 {
- return nil
- }
- return uncommented[seenLine : lastContent+1]
- }
-
- // a shared function that can be used to split given headers
- // into a title and description
- func collectScannerTitleDescription(headers []string) (title, desc []string) {
- hdrs := cleanupScannerLines(headers, rxUncommentHeaders, nil)
-
- idx := -1
- for i, line := range hdrs {
- if strings.TrimSpace(line) == "" {
- idx = i
- break
- }
- }
-
- if idx > -1 {
- title = hdrs[:idx]
- if len(hdrs) > idx+1 {
- desc = hdrs[idx+1:]
- } else {
- desc = nil
- }
- return
- }
-
- if len(hdrs) > 0 {
- line := hdrs[0]
- if rxPunctuationEnd.MatchString(line) {
- title = []string{line}
- desc = hdrs[1:]
- } else {
- desc = hdrs
- }
- }
-
- return
- }
-
- func (sp *yamlSpecScanner) collectTitleDescription() {
- if sp.workedOutTitle {
- return
- }
- if sp.setTitle == nil {
- sp.header = cleanupScannerLines(sp.header, rxUncommentHeaders, nil)
- return
- }
-
- sp.workedOutTitle = true
- sp.title, sp.header = collectScannerTitleDescription(sp.header)
- }
-
- func (sp *yamlSpecScanner) Title() []string {
- sp.collectTitleDescription()
- return sp.title
- }
-
- func (sp *yamlSpecScanner) Description() []string {
- sp.collectTitleDescription()
- return sp.header
- }
-
- func (sp *yamlSpecScanner) Parse(doc *ast.CommentGroup) error {
- if doc == nil {
- return nil
- }
- var startedYAMLSpec bool
- COMMENTS:
- for _, c := range doc.List {
- for _, line := range strings.Split(c.Text, "\n") {
- if rxSwaggerAnnotation.MatchString(line) {
- break COMMENTS // a new swagger: annotation terminates this parser
- }
-
- if !startedYAMLSpec {
- if rxBeginYAMLSpec.MatchString(line) {
- startedYAMLSpec = true
- sp.yamlSpec = append(sp.yamlSpec, line)
- continue
- }
-
- if !sp.skipHeader {
- sp.header = append(sp.header, line)
- }
-
- // no YAML spec yet, moving on
- continue
- }
-
- sp.yamlSpec = append(sp.yamlSpec, line)
- }
- }
- if sp.setTitle != nil {
- sp.setTitle(sp.Title())
- }
- if sp.setDescription != nil {
- sp.setDescription(sp.Description())
- }
- return nil
- }
-
- func (sp *yamlSpecScanner) UnmarshalSpec(u func([]byte) error) (err error) {
- spec := cleanupScannerLines(sp.yamlSpec, rxUncommentYAML, nil)
- if len(spec) == 0 {
- return errors.New("no spec available to unmarshal")
- }
-
- if !strings.Contains(spec[0], "---") {
- return errors.New("yaml spec has to start with `---`")
- }
-
- // remove indentation
- spec = removeIndent(spec)
-
- // 1. parse yaml lines
- yamlValue := make(map[interface{}]interface{})
-
- yamlContent := strings.Join(spec, "\n")
- err = yaml.Unmarshal([]byte(yamlContent), &yamlValue)
- if err != nil {
- return
- }
-
- // 2. convert to json
- var jsonValue json.RawMessage
- jsonValue, err = fmts.YAMLToJSON(yamlValue)
- if err != nil {
- return
- }
-
- // 3. unmarshal the json into an interface
- var data []byte
- data, err = jsonValue.MarshalJSON()
- if err != nil {
- return
- }
- err = u(data)
- if err != nil {
- return
- }
-
- // all parsed, returning...
- sp.yamlSpec = nil // spec is now consumed, so let's erase the parsed lines
- return
- }
-
- // removes indent base on the first line
- func removeIndent(spec []string) []string {
- loc := rxIndent.FindStringIndex(spec[0])
- if loc[1] > 0 {
- for i := range spec {
- if len(spec[i]) >= loc[1] {
- spec[i] = spec[i][loc[1]-1:]
- }
- }
- }
- return spec
- }
-
- // removes indent base on the first line
- func removeYamlIndent(spec []string) []string {
- loc := rxIndent.FindStringIndex(spec[0])
- var s []string
- if loc[1] > 0 {
- for i := range spec {
- if len(spec[i]) >= loc[1] {
- s = append(s, spec[i][loc[1]-1:])
- }
- }
- }
- return s
- }
-
- // aggregates lines in header until it sees a tag.
- type sectionedParser struct {
- header []string
- matched map[string]tagParser
- annotation valueParser
-
- seenTag bool
- skipHeader bool
- setTitle func([]string)
- setDescription func([]string)
- workedOutTitle bool
- taggers []tagParser
- currentTagger *tagParser
- title []string
- ignored bool
- }
-
- func (st *sectionedParser) collectTitleDescription() {
- if st.workedOutTitle {
- return
- }
- if st.setTitle == nil {
- st.header = cleanupScannerLines(st.header, rxUncommentHeaders, nil)
- return
- }
-
- st.workedOutTitle = true
- st.title, st.header = collectScannerTitleDescription(st.header)
- }
-
- func (st *sectionedParser) Title() []string {
- st.collectTitleDescription()
- return st.title
- }
-
- func (st *sectionedParser) Description() []string {
- st.collectTitleDescription()
- return st.header
- }
-
- func (st *sectionedParser) Parse(doc *ast.CommentGroup) error {
- if doc == nil {
- return nil
- }
- COMMENTS:
- for _, c := range doc.List {
- for _, line := range strings.Split(c.Text, "\n") {
- if rxSwaggerAnnotation.MatchString(line) {
- if rxIgnoreOverride.MatchString(line) {
- st.ignored = true
- break COMMENTS // an explicit ignore terminates this parser
- }
- if st.annotation == nil || !st.annotation.Matches(line) {
- break COMMENTS // a new swagger: annotation terminates this parser
- }
-
- _ = st.annotation.Parse([]string{line})
- if len(st.header) > 0 {
- st.seenTag = true
- }
- continue
- }
-
- var matched bool
- for _, tagger := range st.taggers {
- if tagger.Matches(line) {
- st.seenTag = true
- st.currentTagger = &tagger
- matched = true
- break
- }
- }
-
- if st.currentTagger == nil {
- if !st.skipHeader && !st.seenTag {
- st.header = append(st.header, line)
- }
- // didn't match a tag, moving on
- continue
- }
-
- if st.currentTagger.MultiLine && matched {
- // the first line of a multiline tagger doesn't count
- continue
- }
-
- ts, ok := st.matched[st.currentTagger.Name]
- if !ok {
- ts = *st.currentTagger
- }
- ts.Lines = append(ts.Lines, line)
- if st.matched == nil {
- st.matched = make(map[string]tagParser)
- }
- st.matched[st.currentTagger.Name] = ts
-
- if !st.currentTagger.MultiLine {
- st.currentTagger = nil
- }
- }
- }
- if st.setTitle != nil {
- st.setTitle(st.Title())
- }
- if st.setDescription != nil {
- st.setDescription(st.Description())
- }
- for _, mt := range st.matched {
- if !mt.SkipCleanUp {
- mt.Lines = cleanupScannerLines(mt.Lines, rxUncommentHeaders, nil)
- }
- if err := mt.Parse(mt.Lines); err != nil {
- return err
- }
- }
- return nil
- }
|