github.com/kaisawind/go-swagger@v0.19.0/scan/scanner.go (about)

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