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  }