mirror of https://github.com/go-gitea/gitea.git
952 lines
25 KiB
Go
952 lines
25 KiB
Go
// +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
|
|
}
|