|
- // +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"
- "fmt"
- "go/ast"
- "log"
- "os"
- "path/filepath"
- "reflect"
- "runtime"
- "strconv"
- "strings"
-
- "golang.org/x/tools/go/loader"
-
- "github.com/go-openapi/spec"
- )
-
- func addExtension(ve *spec.VendorExtensible, key string, value interface{}) {
- if os.Getenv("SWAGGER_GENERATE_EXTENSION") == "false" {
- return
- }
-
- ve.AddExtension(key, value)
- }
-
- type schemaTypable struct {
- schema *spec.Schema
- level int
- }
-
- func (st schemaTypable) Typed(tpe, format string) {
- st.schema.Typed(tpe, format)
- }
-
- func (st schemaTypable) SetRef(ref spec.Ref) {
- st.schema.Ref = ref
- }
-
- func (st schemaTypable) Schema() *spec.Schema {
- return st.schema
- }
-
- func (st schemaTypable) Items() swaggerTypable {
- if st.schema.Items == nil {
- st.schema.Items = new(spec.SchemaOrArray)
- }
- if st.schema.Items.Schema == nil {
- st.schema.Items.Schema = new(spec.Schema)
- }
-
- st.schema.Typed("array", "")
- return schemaTypable{st.schema.Items.Schema, st.level + 1}
- }
-
- func (st schemaTypable) AdditionalProperties() swaggerTypable {
- if st.schema.AdditionalProperties == nil {
- st.schema.AdditionalProperties = new(spec.SchemaOrBool)
- }
- if st.schema.AdditionalProperties.Schema == nil {
- st.schema.AdditionalProperties.Schema = new(spec.Schema)
- }
-
- st.schema.Typed("object", "")
- return schemaTypable{st.schema.AdditionalProperties.Schema, st.level + 1}
- }
- func (st schemaTypable) Level() int { return st.level }
-
- type schemaValidations struct {
- current *spec.Schema
- }
-
- func (sv schemaValidations) SetMaximum(val float64, exclusive bool) {
- sv.current.Maximum = &val
- sv.current.ExclusiveMaximum = exclusive
- }
- func (sv schemaValidations) SetMinimum(val float64, exclusive bool) {
- sv.current.Minimum = &val
- sv.current.ExclusiveMinimum = exclusive
- }
- func (sv schemaValidations) SetMultipleOf(val float64) { sv.current.MultipleOf = &val }
- func (sv schemaValidations) SetMinItems(val int64) { sv.current.MinItems = &val }
- func (sv schemaValidations) SetMaxItems(val int64) { sv.current.MaxItems = &val }
- func (sv schemaValidations) SetMinLength(val int64) { sv.current.MinLength = &val }
- func (sv schemaValidations) SetMaxLength(val int64) { sv.current.MaxLength = &val }
- func (sv schemaValidations) SetPattern(val string) { sv.current.Pattern = val }
- func (sv schemaValidations) SetUnique(val bool) { sv.current.UniqueItems = val }
- func (sv schemaValidations) SetDefault(val interface{}) { sv.current.Default = val }
- func (sv schemaValidations) SetExample(val interface{}) { sv.current.Example = val }
- func (sv schemaValidations) SetEnum(val string) {
- sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Format: sv.current.Format, Type: sv.current.Type[0]})
- }
-
- type schemaDecl struct {
- File *ast.File
- Decl *ast.GenDecl
- TypeSpec *ast.TypeSpec
- GoName string
- Name string
- annotated bool
- }
-
- func newSchemaDecl(file *ast.File, decl *ast.GenDecl, ts *ast.TypeSpec) *schemaDecl {
- sd := &schemaDecl{
- File: file,
- Decl: decl,
- TypeSpec: ts,
- }
- sd.inferNames()
- return sd
- }
-
- func (sd *schemaDecl) hasAnnotation() bool {
- sd.inferNames()
- return sd.annotated
- }
-
- func (sd *schemaDecl) inferNames() (goName string, name string) {
- if sd.GoName != "" {
- goName, name = sd.GoName, sd.Name
- return
- }
- goName = sd.TypeSpec.Name.Name
- name = goName
- if sd.Decl.Doc != nil {
- DECLS:
- for _, cmt := range sd.Decl.Doc.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxModelOverride.FindStringSubmatch(ln)
- if len(matches) > 0 {
- sd.annotated = true
- }
- if len(matches) > 1 && len(matches[1]) > 0 {
- name = matches[1]
- break DECLS
- }
- }
- }
- }
- sd.GoName = goName
- sd.Name = name
- return
- }
-
- type schemaParser struct {
- program *loader.Program
- postDecls []schemaDecl
- known map[string]spec.Schema
- discovered *schemaDecl
- }
-
- func newSchemaParser(prog *loader.Program) *schemaParser {
- scp := new(schemaParser)
- scp.program = prog
- scp.known = make(map[string]spec.Schema)
- return scp
- }
-
- func (scp *schemaParser) Parse(gofile *ast.File, target interface{}) error {
- tgt := target.(map[string]spec.Schema)
- for _, decl := range gofile.Decls {
- gd, ok := decl.(*ast.GenDecl)
- if !ok {
- continue
- }
- for _, spc := range gd.Specs {
- if ts, ok := spc.(*ast.TypeSpec); ok {
- sd := newSchemaDecl(gofile, gd, ts)
- if err := scp.parseDecl(tgt, sd); err != nil {
- return err
- }
- }
- }
- }
- return nil
- }
-
- func (scp *schemaParser) parseDecl(definitions map[string]spec.Schema, decl *schemaDecl) error {
- // check if there is a swagger:model tag that is followed by a word,
- // this word is the type name for swagger
- // the package and type are recorded in the extensions
- // once type name is found convert it to a schema, by looking up the schema in the
- // definitions dictionary that got passed into this parse method
-
- // if our schemaParser is parsing a discovered schemaDecl and it does not match
- // the current schemaDecl we can skip parsing.
- if scp.discovered != nil && scp.discovered.Name != decl.Name {
- return nil
- }
-
- decl.inferNames()
- schema := definitions[decl.Name]
- schPtr := &schema
-
- // analyze doc comment for the model
- sp := new(sectionedParser)
- sp.setTitle = func(lines []string) { schema.Title = joinDropLast(lines) }
- sp.setDescription = func(lines []string) { schema.Description = joinDropLast(lines) }
- if err := sp.Parse(decl.Decl.Doc); err != nil {
- return err
- }
-
- // if the type is marked to ignore, just return
- if sp.ignored {
- return nil
- }
-
- // analyze struct body for fields etc
- // each exported struct field:
- // * gets a type mapped to a go primitive
- // * perhaps gets a format
- // * has to document the validations that apply for the type and the field
- // * when the struct field points to a model it becomes a ref: #/definitions/ModelName
- // * the first line of the comment is the title
- // * the following lines are the description
- switch tpe := decl.TypeSpec.Type.(type) {
- case *ast.StructType:
- if err := scp.parseStructType(decl.File, schPtr, tpe, make(map[string]string)); err != nil {
- return err
- }
- case *ast.InterfaceType:
- if err := scp.parseInterfaceType(decl.File, schPtr, tpe, make(map[string]string)); err != nil {
- return err
- }
- case *ast.Ident:
- prop := &schemaTypable{schPtr, 0}
- if strfmtName, ok := strfmtName(decl.Decl.Doc); ok {
- prop.Typed("string", strfmtName)
- } else {
- if err := scp.parseNamedType(decl.File, tpe, prop); err != nil {
- return err
- }
- }
- case *ast.SelectorExpr:
- prop := &schemaTypable{schPtr, 0}
- if strfmtName, ok := strfmtName(decl.Decl.Doc); ok {
- prop.Typed("string", strfmtName)
- } else {
- if err := scp.parseNamedType(decl.File, tpe, prop); err != nil {
- return err
- }
- }
-
- case *ast.ArrayType:
- prop := &schemaTypable{schPtr, 0}
- if strfmtName, ok := strfmtName(decl.Decl.Doc); ok {
- prop.Items().Typed("string", strfmtName)
- } else {
- if err := scp.parseNamedType(decl.File, tpe, &schemaTypable{schPtr, 0}); err != nil {
- return err
- }
- }
-
- case *ast.MapType:
- prop := &schemaTypable{schPtr, 0}
- if strfmtName, ok := strfmtName(decl.Decl.Doc); ok {
- prop.AdditionalProperties().Typed("string", strfmtName)
- } else {
- if err := scp.parseNamedType(decl.File, tpe, &schemaTypable{schPtr, 0}); err != nil {
- return err
- }
- }
- default:
- log.Printf("WARNING: Missing parser for a %T, skipping model: %s\n", tpe, decl.Name)
- return nil
- }
-
- if schPtr.Ref.String() == "" {
- if decl.Name != decl.GoName {
- addExtension(&schPtr.VendorExtensible, "x-go-name", decl.GoName)
- }
- for _, pkgInfo := range scp.program.AllPackages {
- if pkgInfo.Importable {
- for _, fil := range pkgInfo.Files {
- if fil.Pos() == decl.File.Pos() {
- addExtension(&schPtr.VendorExtensible, "x-go-package", pkgInfo.Pkg.Path())
- }
- }
- }
- }
- }
- definitions[decl.Name] = schema
- return nil
- }
-
- func (scp *schemaParser) parseNamedType(gofile *ast.File, expr ast.Expr, prop swaggerTypable) error {
- switch ftpe := expr.(type) {
- case *ast.Ident: // simple value
- pkg, err := scp.packageForFile(gofile, ftpe)
- if err != nil {
- return err
- }
- return scp.parseIdentProperty(pkg, ftpe, prop)
-
- case *ast.StarExpr: // pointer to something, optional by default
- if err := scp.parseNamedType(gofile, ftpe.X, prop); err != nil {
- return err
- }
-
- case *ast.ArrayType: // slice type
- if err := scp.parseNamedType(gofile, ftpe.Elt, prop.Items()); err != nil {
- return err
- }
-
- case *ast.StructType:
- schema := prop.Schema()
- if schema == nil {
- return fmt.Errorf("items doesn't support embedded structs")
- }
- return scp.parseStructType(gofile, prop.Schema(), ftpe, make(map[string]string))
-
- case *ast.SelectorExpr:
- err := scp.typeForSelector(gofile, ftpe, prop)
- return err
-
- case *ast.MapType:
- // check if key is a string type, if not print a message
- // and skip the map property. Only maps with string keys can go into additional properties
- sch := prop.Schema()
- if sch == nil {
- return fmt.Errorf("items doesn't support maps")
- }
- if keyIdent, ok := ftpe.Key.(*ast.Ident); sch != nil && ok {
- if keyIdent.Name == "string" {
- if sch.AdditionalProperties == nil {
- sch.AdditionalProperties = new(spec.SchemaOrBool)
- }
- sch.AdditionalProperties.Allows = false
- if sch.AdditionalProperties.Schema == nil {
- sch.AdditionalProperties.Schema = new(spec.Schema)
- }
- if err := scp.parseNamedType(gofile, ftpe.Value, schemaTypable{sch.AdditionalProperties.Schema, 0}); err != nil {
- return err
- }
- sch.Typed("object", "")
- }
- }
-
- case *ast.InterfaceType:
- prop.Schema().Typed("object", "")
- default:
- pos := "unknown file:unknown position"
- if scp != nil {
- if scp.program != nil {
- if scp.program.Fset != nil {
- pos = scp.program.Fset.Position(expr.Pos()).String()
- }
- }
- }
- return fmt.Errorf("expr (%s) is unsupported for a schema", pos)
- }
- return nil
- }
-
- func (scp *schemaParser) parseEmbeddedType(gofile *ast.File, schema *spec.Schema, expr ast.Expr, seenPreviously map[string]string) error {
- switch tpe := expr.(type) {
- case *ast.Ident:
- // do lookup of type
- // take primitives into account, they should result in an error for swagger
- pkg, err := scp.packageForFile(gofile, tpe)
- if err != nil {
- return err
- }
- file, _, ts, err := findSourceFile(pkg, tpe.Name)
- if err != nil {
- return err
- }
-
- switch st := ts.Type.(type) {
- case *ast.StructType:
- return scp.parseStructType(file, schema, st, seenPreviously)
- case *ast.InterfaceType:
- return scp.parseInterfaceType(file, schema, st, seenPreviously)
- default:
- prop := &schemaTypable{schema, 0}
- return scp.parseNamedType(gofile, st, prop)
- }
-
- case *ast.SelectorExpr:
- // look up package, file and then type
- pkg, err := scp.packageForSelector(gofile, tpe.X)
- if err != nil {
- return fmt.Errorf("embedded struct: %v", err)
- }
- file, _, ts, err := findSourceFile(pkg, tpe.Sel.Name)
- if err != nil {
- return fmt.Errorf("embedded struct: %v", err)
- }
- if st, ok := ts.Type.(*ast.StructType); ok {
- return scp.parseStructType(file, schema, st, seenPreviously)
- }
- if st, ok := ts.Type.(*ast.InterfaceType); ok {
- return scp.parseInterfaceType(file, schema, st, seenPreviously)
- }
- case *ast.StarExpr:
- return scp.parseEmbeddedType(gofile, schema, tpe.X, seenPreviously)
- default:
- return fmt.Errorf(
- "parseEmbeddedType: unsupported type %v at position %#v",
- expr,
- scp.program.Fset.Position(tpe.Pos()),
- )
- }
- return fmt.Errorf("unable to resolve embedded struct for: %v", expr)
- }
-
- func (scp *schemaParser) parseAllOfMember(gofile *ast.File, schema *spec.Schema, expr ast.Expr, seenPreviously map[string]string) error {
- // TODO: check if struct is annotated with swagger:model or known in the definitions otherwise
- var pkg *loader.PackageInfo
- var file *ast.File
- var gd *ast.GenDecl
- var ts *ast.TypeSpec
- var err error
-
- switch tpe := expr.(type) {
- case *ast.Ident:
- // do lookup of type
- // take primitives into account, they should result in an error for swagger
- pkg, err = scp.packageForFile(gofile, tpe)
- if err != nil {
- return err
- }
- file, gd, ts, err = findSourceFile(pkg, tpe.Name)
- if err != nil {
- return err
- }
-
- case *ast.SelectorExpr:
- // look up package, file and then type
- pkg, err = scp.packageForSelector(gofile, tpe.X)
- if err != nil {
- return fmt.Errorf("embedded struct: %v", err)
- }
- file, gd, ts, err = findSourceFile(pkg, tpe.Sel.Name)
- if err != nil {
- return fmt.Errorf("embedded struct: %v", err)
- }
- default:
- return fmt.Errorf("unable to resolve allOf member for: %v", expr)
- }
-
- sd := newSchemaDecl(file, gd, ts)
- if sd.hasAnnotation() && pkg.String() != "time" && ts.Name.Name != "Time" {
- ref, err := spec.NewRef("#/definitions/" + sd.Name)
- if err != nil {
- return err
- }
- schema.Ref = ref
- scp.postDecls = append(scp.postDecls, *sd)
- } else {
- switch st := ts.Type.(type) {
- case *ast.StructType:
- return scp.parseStructType(file, schema, st, seenPreviously)
- case *ast.InterfaceType:
- return scp.parseInterfaceType(file, schema, st, seenPreviously)
- }
- }
-
- return nil
- }
- func (scp *schemaParser) parseInterfaceType(gofile *ast.File, bschema *spec.Schema, tpe *ast.InterfaceType, seenPreviously map[string]string) error {
- if tpe.Methods == nil {
- return nil
- }
-
- // first check if this has embedded interfaces, if so make sure to refer to those by ref
- // when they are decorated with an allOf annotation
- // go over the method list again and this time collect the nullary methods and parse the comments
- // as if they are properties on a struct
- var schema *spec.Schema
- seenProperties := seenPreviously
- hasAllOf := false
-
- for _, fld := range tpe.Methods.List {
- if len(fld.Names) == 0 {
- // if this created an allOf property then we have to rejig the schema var
- // because all the fields collected that aren't from embedded structs should go in
- // their own proper schema
- // first process embedded structs in order of embedding
- if allOfMember(fld.Doc) {
- hasAllOf = true
- if schema == nil {
- schema = new(spec.Schema)
- }
- var newSch spec.Schema
- // when the embedded struct is annotated with swagger:allOf it will be used as allOf property
- // otherwise the fields will just be included as normal properties
- if err := scp.parseAllOfMember(gofile, &newSch, fld.Type, seenProperties); err != nil {
- return err
- }
-
- if fld.Doc != nil {
- for _, cmt := range fld.Doc.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxAllOf.FindStringSubmatch(ln)
- ml := len(matches)
- if ml > 1 {
- mv := matches[ml-1]
- if mv != "" {
- addExtension(&bschema.VendorExtensible, "x-class", mv)
- }
- }
- }
- }
- }
-
- bschema.AllOf = append(bschema.AllOf, newSch)
- continue
- }
-
- var newSch spec.Schema
- // when the embedded struct is annotated with swagger:allOf it will be used as allOf property
- // otherwise the fields will just be included as normal properties
- if err := scp.parseEmbeddedType(gofile, &newSch, fld.Type, seenProperties); err != nil {
- return err
- }
- bschema.AllOf = append(bschema.AllOf, newSch)
- hasAllOf = true
- }
- }
-
- if schema == nil {
- schema = bschema
- }
- // then add and possibly override values
- if schema.Properties == nil {
- schema.Properties = make(map[string]spec.Schema)
- }
- schema.Typed("object", "")
- for _, fld := range tpe.Methods.List {
- if mtpe, ok := fld.Type.(*ast.FuncType); ok && mtpe.Params.NumFields() == 0 && mtpe.Results.NumFields() == 1 {
- gnm := fld.Names[0].Name
- nm := gnm
- if fld.Doc != nil {
- for _, cmt := range fld.Doc.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxName.FindStringSubmatch(ln)
- ml := len(matches)
- if ml > 1 {
- nm = matches[ml-1]
- }
- }
- }
- }
-
- ps := schema.Properties[nm]
- if err := parseProperty(scp, gofile, mtpe.Results.List[0].Type, schemaTypable{&ps, 0}); err != nil {
- return err
- }
-
- if err := scp.createParser(nm, schema, &ps, fld).Parse(fld.Doc); err != nil {
- return err
- }
-
- if ps.Ref.String() == "" && nm != gnm {
- addExtension(&ps.VendorExtensible, "x-go-name", gnm)
- }
- seenProperties[nm] = gnm
- schema.Properties[nm] = ps
- }
-
- }
- if schema != nil && hasAllOf && len(schema.Properties) > 0 {
- bschema.AllOf = append(bschema.AllOf, *schema)
- }
- for k := range schema.Properties {
- if _, ok := seenProperties[k]; !ok {
- delete(schema.Properties, k)
- }
- }
- return nil
- }
-
- func (scp *schemaParser) parseStructType(gofile *ast.File, bschema *spec.Schema, tpe *ast.StructType, seenPreviously map[string]string) error {
- if tpe.Fields == nil {
- return nil
- }
- var schema *spec.Schema
- seenProperties := seenPreviously
- hasAllOf := false
-
- for _, fld := range tpe.Fields.List {
- if len(fld.Names) == 0 {
- // if the field is annotated with swagger:ignore, ignore it
- if ignored(fld.Doc) {
- continue
- }
-
- _, ignore, _, err := parseJSONTag(fld)
- if err != nil {
- return err
- }
- if ignore {
- continue
- }
-
- // if this created an allOf property then we have to rejig the schema var
- // because all the fields collected that aren't from embedded structs should go in
- // their own proper schema
- // first process embedded structs in order of embedding
- if allOfMember(fld.Doc) {
- hasAllOf = true
- if schema == nil {
- schema = new(spec.Schema)
- }
- var newSch spec.Schema
- // when the embedded struct is annotated with swagger:allOf it will be used as allOf property
- // otherwise the fields will just be included as normal properties
- if err := scp.parseAllOfMember(gofile, &newSch, fld.Type, seenProperties); err != nil {
- return err
- }
-
- if fld.Doc != nil {
- for _, cmt := range fld.Doc.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxAllOf.FindStringSubmatch(ln)
- ml := len(matches)
- if ml > 1 {
- mv := matches[ml-1]
- if mv != "" {
- addExtension(&bschema.VendorExtensible, "x-class", mv)
- }
- }
- }
- }
- }
-
- bschema.AllOf = append(bschema.AllOf, newSch)
- continue
- }
- if schema == nil {
- schema = bschema
- }
-
- // when the embedded struct is annotated with swagger:allOf it will be used as allOf property
- // otherwise the fields will just be included as normal properties
- if err := scp.parseEmbeddedType(gofile, schema, fld.Type, seenProperties); err != nil {
- return err
- }
- }
- }
- if schema == nil {
- schema = bschema
- }
-
- // then add and possibly override values
- if schema.Properties == nil {
- schema.Properties = make(map[string]spec.Schema)
- }
- schema.Typed("object", "")
- for _, fld := range tpe.Fields.List {
- if len(fld.Names) > 0 && fld.Names[0] != nil && fld.Names[0].IsExported() {
- // if the field is annotated with swagger:ignore, ignore it
- if ignored(fld.Doc) {
- continue
- }
-
- gnm := fld.Names[0].Name
- nm, ignore, isString, err := parseJSONTag(fld)
- if err != nil {
- return err
- }
- if ignore {
- for seenTagName, seenFieldName := range seenPreviously {
- if seenFieldName == gnm {
- delete(schema.Properties, seenTagName)
- break
- }
- }
- continue
- }
-
- ps := schema.Properties[nm]
- if err := parseProperty(scp, gofile, fld.Type, schemaTypable{&ps, 0}); err != nil {
- return err
- }
- if isString {
- ps.Typed("string", ps.Format)
- ps.Ref = spec.Ref{}
- }
- if strfmtName, ok := strfmtName(fld.Doc); ok {
- ps.Typed("string", strfmtName)
- ps.Ref = spec.Ref{}
- }
-
- if err := scp.createParser(nm, schema, &ps, fld).Parse(fld.Doc); err != nil {
- return err
- }
-
- if ps.Ref.String() == "" && nm != gnm {
- addExtension(&ps.VendorExtensible, "x-go-name", gnm)
- }
- // we have 2 cases:
- // 1. field with different name override tag
- // 2. field with different name removes tag
- // so we need to save both tag&name
- seenProperties[nm] = gnm
- schema.Properties[nm] = ps
- }
- }
- if schema != nil && hasAllOf && len(schema.Properties) > 0 {
- bschema.AllOf = append(bschema.AllOf, *schema)
- }
- for k := range schema.Properties {
- if _, ok := seenProperties[k]; !ok {
- delete(schema.Properties, k)
- }
- }
- return nil
- }
-
- func schemaVendorExtensibleSetter(meta *spec.Schema) func(json.RawMessage) error {
- return func(jsonValue json.RawMessage) error {
- var jsonData spec.Extensions
- err := json.Unmarshal(jsonValue, &jsonData)
- if err != nil {
- return err
- }
- for k := range jsonData {
- if !rxAllowedExtensions.MatchString(k) {
- return fmt.Errorf("invalid schema extension name, should start from `x-`: %s", k)
- }
- }
- meta.Extensions = jsonData
- return nil
- }
- }
-
- func (scp *schemaParser) createParser(nm string, schema, ps *spec.Schema, fld *ast.Field) *sectionedParser {
- sp := new(sectionedParser)
-
- schemeType, err := ps.Type.MarshalJSON()
- if err != nil {
- return nil
- }
-
- if ps.Ref.String() == "" {
- sp.setDescription = func(lines []string) { ps.Description = joinDropLast(lines) }
- sp.taggers = []tagParser{
- newSingleLineTagParser("maximum", &setMaximum{schemaValidations{ps}, rxf(rxMaximumFmt, "")}),
- newSingleLineTagParser("minimum", &setMinimum{schemaValidations{ps}, rxf(rxMinimumFmt, "")}),
- newSingleLineTagParser("multipleOf", &setMultipleOf{schemaValidations{ps}, rxf(rxMultipleOfFmt, "")}),
- newSingleLineTagParser("minLength", &setMinLength{schemaValidations{ps}, rxf(rxMinLengthFmt, "")}),
- newSingleLineTagParser("maxLength", &setMaxLength{schemaValidations{ps}, rxf(rxMaxLengthFmt, "")}),
- newSingleLineTagParser("pattern", &setPattern{schemaValidations{ps}, rxf(rxPatternFmt, "")}),
- newSingleLineTagParser("minItems", &setMinItems{schemaValidations{ps}, rxf(rxMinItemsFmt, "")}),
- newSingleLineTagParser("maxItems", &setMaxItems{schemaValidations{ps}, rxf(rxMaxItemsFmt, "")}),
- newSingleLineTagParser("unique", &setUnique{schemaValidations{ps}, rxf(rxUniqueFmt, "")}),
- newSingleLineTagParser("enum", &setEnum{schemaValidations{ps}, rxf(rxEnumFmt, "")}),
- newSingleLineTagParser("default", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}),
- newSingleLineTagParser("type", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}),
- newSingleLineTagParser("example", &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxExampleFmt, "")}),
- newSingleLineTagParser("required", &setRequiredSchema{schema, nm}),
- newSingleLineTagParser("readOnly", &setReadOnlySchema{ps}),
- newSingleLineTagParser("discriminator", &setDiscriminator{schema, nm}),
- newMultiLineTagParser("YAMLExtensionsBlock", newYamlParser(rxExtensions, schemaVendorExtensibleSetter(ps)), true),
- }
-
- itemsTaggers := func(items *spec.Schema, level int) []tagParser {
- schemeType, err := items.Type.MarshalJSON()
- if err != nil {
- return nil
- }
- // the expression is 1-index based not 0-index
- itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1)
- return []tagParser{
- newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{schemaValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{schemaValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{schemaValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{schemaValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{schemaValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{schemaValidations{items}, rxf(rxPatternFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{schemaValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{schemaValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{schemaValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{schemaValidations{items}, rxf(rxEnumFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}),
- newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxExampleFmt, itemsPrefix)}),
- }
- }
-
- var parseArrayTypes func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error)
- parseArrayTypes = func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error) {
- if items == nil || items.Schema == nil {
- return []tagParser{}, nil
- }
- switch iftpe := expr.(type) {
- case *ast.ArrayType:
- eleTaggers := itemsTaggers(items.Schema, level)
- sp.taggers = append(eleTaggers, sp.taggers...)
- otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Schema.Items, level+1)
- if err != nil {
- return nil, err
- }
- return otherTaggers, nil
- case *ast.Ident:
- taggers := []tagParser{}
- if iftpe.Obj == nil {
- taggers = itemsTaggers(items.Schema, level)
- }
- otherTaggers, err := parseArrayTypes(expr, items.Schema.Items, level+1)
- if err != nil {
- return nil, err
- }
- return append(taggers, otherTaggers...), nil
- case *ast.StarExpr:
- otherTaggers, err := parseArrayTypes(iftpe.X, items, level)
- if err != nil {
- return nil, err
- }
- return otherTaggers, nil
- default:
- return nil, fmt.Errorf("unknown field type ele for %q", nm)
- }
- }
- // check if this is a primitive, if so parse the validations from the
- // doc comments of the slice declaration.
- if ftped, ok := fld.Type.(*ast.ArrayType); ok {
- taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0)
- if err != nil {
- return sp
- }
- sp.taggers = append(taggers, sp.taggers...)
- }
-
- } else {
- sp.taggers = []tagParser{
- newSingleLineTagParser("required", &setRequiredSchema{schema, nm}),
- }
- }
- return sp
- }
-
- // hasFilePathPrefix reports whether the filesystem path s begins with the
- // elements in prefix.
- //
- // taken from: https://github.com/golang/go/blob/c87520c5981ecdeaa99e7ba636a6088f900c0c75/src/cmd/go/internal/load/path.go#L60-L80
- func hasFilePathPrefix(s, prefix string) bool {
- sv := strings.ToUpper(filepath.VolumeName(s))
- pv := strings.ToUpper(filepath.VolumeName(prefix))
- s = s[len(sv):]
- prefix = prefix[len(pv):]
- switch {
- default:
- return false
- case sv != pv:
- return false
- case len(s) == len(prefix):
- return s == prefix
- case len(s) > len(prefix):
- if prefix != "" && prefix[len(prefix)-1] == filepath.Separator {
- return strings.HasPrefix(s, prefix)
- }
- return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix
- }
- }
-
- func (scp *schemaParser) packageForFile(gofile *ast.File, tpe *ast.Ident) (*loader.PackageInfo, error) {
- fn := scp.program.Fset.File(gofile.Pos()).Name()
- if Debug {
- log.Println("trying for", fn, tpe.Name, tpe.String())
- }
- fa, err := filepath.Abs(fn)
- if err != nil {
- return nil, err
- }
- if Debug {
- log.Println("absolute path", fa)
- }
- var fgp string
- gopath := os.Getenv("GOPATH")
- if gopath == "" {
- gopath = filepath.Join(os.Getenv("HOME"), "go")
- }
- for _, p := range append(filepath.SplitList(gopath), runtime.GOROOT()) {
- pref := filepath.Join(p, "src")
- if hasFilePathPrefix(fa, pref) {
- fgp = filepath.Dir(strings.TrimPrefix(fa, pref))[1:]
- break
- }
- }
- if Debug {
- log.Println("package in gopath", fgp)
- }
- for pkg, pkgInfo := range scp.program.AllPackages {
- if Debug {
- log.Println("inferring for", tpe.Name, "with", gofile.Name.Name, "at", pkg.Path(), "against", filepath.ToSlash(fgp))
- }
- if pkg.Name() == gofile.Name.Name && filepath.ToSlash(fgp) == pkg.Path() {
- return pkgInfo, nil
- }
- }
-
- return nil, fmt.Errorf("unable to determine package for %s", fn)
- }
-
- func (scp *schemaParser) packageForSelector(gofile *ast.File, expr ast.Expr) (*loader.PackageInfo, error) {
-
- if pth, ok := expr.(*ast.Ident); ok {
- // lookup import
- var selPath string
- for _, imp := range gofile.Imports {
- pv, err := strconv.Unquote(imp.Path.Value)
- if err != nil {
- pv = imp.Path.Value
- }
- if imp.Name != nil {
- if imp.Name.Name == pth.Name {
- selPath = pv
- break
- }
- } else {
- pkg := scp.program.Package(pv)
- if pkg != nil && pth.Name == pkg.Pkg.Name() {
- selPath = pv
- break
- } else {
- parts := strings.Split(pv, "/")
- if len(parts) > 0 && parts[len(parts)-1] == pth.Name {
- selPath = pv
- break
- }
- }
- }
- }
- // find actual struct
- if selPath == "" {
- return nil, fmt.Errorf("no import found for %s", pth.Name)
- }
-
- pkg := scp.program.Package(selPath)
- if pkg != nil {
- return pkg, nil
- }
- // TODO: I must admit this made me cry, it's not even a great solution.
- pkg = scp.program.Package("github.com/go-swagger/go-swagger/vendor/" + selPath)
- if pkg != nil {
- return pkg, nil
- }
- for _, info := range scp.program.AllPackages {
- n := info.String()
- path := "/vendor/" + selPath
- if strings.HasSuffix(n, path) {
- pkg = scp.program.Package(n)
- return pkg, nil
- }
- }
- }
- return nil, fmt.Errorf("can't determine selector path from %v", expr)
- }
-
- func (scp *schemaParser) makeRef(file *ast.File, pkg *loader.PackageInfo, gd *ast.GenDecl, ts *ast.TypeSpec, prop swaggerTypable) error {
- sd := newSchemaDecl(file, gd, ts)
- sd.inferNames()
- // make an exception for time.Time because this is a well-known string format
- if sd.Name == "Time" && pkg.String() == "time" {
- return nil
- }
- ref, err := spec.NewRef("#/definitions/" + sd.Name)
- if err != nil {
- return err
- }
- prop.SetRef(ref)
- scp.postDecls = append(scp.postDecls, *sd)
- return nil
- }
-
- func (scp *schemaParser) parseIdentProperty(pkg *loader.PackageInfo, expr *ast.Ident, prop swaggerTypable) error {
- // before proceeding make an exception to time.Time because it is a well known string format
- if pkg.String() == "time" && expr.String() == "Time" {
- prop.Typed("string", "date-time")
- return nil
- }
-
- // find the file this selector points to
- file, gd, ts, err := findSourceFile(pkg, expr.Name)
-
- if err != nil {
- err := swaggerSchemaForType(expr.Name, prop)
- if err != nil {
- return fmt.Errorf("package %s, error is: %v", pkg.String(), err)
- }
- return nil
- }
-
- if at, ok := ts.Type.(*ast.ArrayType); ok {
- // the swagger spec defines strfmt base64 as []byte.
- // in that case we don't actually want to turn it into an array
- // but we want to turn it into a string
- if _, ok := at.Elt.(*ast.Ident); ok {
- if strfmtName, ok := strfmtName(gd.Doc); ok {
- prop.Typed("string", strfmtName)
- return nil
- }
- }
- // this is a selector, so most likely not base64
- if strfmtName, ok := strfmtName(gd.Doc); ok {
- prop.Items().Typed("string", strfmtName)
- return nil
- }
- }
-
- // look at doc comments for swagger:strfmt [name]
- // when found this is the format name, create a schema with that name
- if strfmtName, ok := strfmtName(gd.Doc); ok {
- prop.Typed("string", strfmtName)
- return nil
- }
-
- if enumName, ok := enumName(gd.Doc); ok {
- log.Println(enumName)
- return nil
- }
-
- if defaultName, ok := defaultName(gd.Doc); ok {
- log.Println(defaultName)
- return nil
- }
-
- if typeName, ok := typeName(gd.Doc); ok {
- _ = swaggerSchemaForType(typeName, prop)
- return nil
- }
-
- if isAliasParam(prop) || aliasParam(gd.Doc) {
- itype, ok := ts.Type.(*ast.Ident)
- if ok {
- err := swaggerSchemaForType(itype.Name, prop)
- if err == nil {
- return nil
- }
- }
- }
- switch tpe := ts.Type.(type) {
- case *ast.ArrayType:
- return scp.makeRef(file, pkg, gd, ts, prop)
- case *ast.StructType:
- return scp.makeRef(file, pkg, gd, ts, prop)
-
- case *ast.Ident:
- return scp.makeRef(file, pkg, gd, ts, prop)
-
- case *ast.StarExpr:
- return parseProperty(scp, file, tpe.X, prop)
-
- case *ast.SelectorExpr:
- // return scp.refForSelector(file, gd, tpe, ts, prop)
- return scp.makeRef(file, pkg, gd, ts, prop)
-
- case *ast.InterfaceType:
- return scp.makeRef(file, pkg, gd, ts, prop)
-
- case *ast.MapType:
- return scp.makeRef(file, pkg, gd, ts, prop)
-
- default:
- err := swaggerSchemaForType(expr.Name, prop)
- if err != nil {
- return fmt.Errorf("package %s, error is: %v", pkg.String(), err)
- }
- return nil
- }
-
- }
-
- func (scp *schemaParser) typeForSelector(gofile *ast.File, expr *ast.SelectorExpr, prop swaggerTypable) error {
- pkg, err := scp.packageForSelector(gofile, expr.X)
- if err != nil {
- return err
- }
-
- return scp.parseIdentProperty(pkg, expr.Sel, prop)
- }
-
- func findSourceFile(pkg *loader.PackageInfo, typeName string) (*ast.File, *ast.GenDecl, *ast.TypeSpec, error) {
- for _, file := range pkg.Files {
- for _, decl := range file.Decls {
- if gd, ok := decl.(*ast.GenDecl); ok {
- for _, gs := range gd.Specs {
- if ts, ok := gs.(*ast.TypeSpec); ok {
- strfmtNme, isStrfmt := strfmtName(gd.Doc)
- if (isStrfmt && strfmtNme == typeName) || ts.Name != nil && ts.Name.Name == typeName {
- return file, gd, ts, nil
- }
- }
- }
- }
- }
- }
- return nil, nil, nil, fmt.Errorf("unable to find %s in %s", typeName, pkg.String())
- }
-
- func allOfMember(comments *ast.CommentGroup) bool {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- if rxAllOf.MatchString(ln) {
- return true
- }
- }
- }
- }
- return false
- }
-
- func fileParam(comments *ast.CommentGroup) bool {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- if rxFileUpload.MatchString(ln) {
- return true
- }
- }
- }
- }
- return false
- }
-
- func strfmtName(comments *ast.CommentGroup) (string, bool) {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxStrFmt.FindStringSubmatch(ln)
- if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 {
- return strings.TrimSpace(matches[1]), true
- }
- }
- }
- }
- return "", false
- }
-
- func ignored(comments *ast.CommentGroup) bool {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- if rxIgnoreOverride.MatchString(ln) {
- return true
- }
- }
- }
- }
- return false
- }
-
- func enumName(comments *ast.CommentGroup) (string, bool) {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxEnum.FindStringSubmatch(ln)
- if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 {
- return strings.TrimSpace(matches[1]), true
- }
- }
- }
- }
- return "", false
- }
-
- func aliasParam(comments *ast.CommentGroup) bool {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- if rxAlias.MatchString(ln) {
- return true
- }
- }
- }
- }
- return false
- }
-
- func defaultName(comments *ast.CommentGroup) (string, bool) {
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxDefault.FindStringSubmatch(ln)
- if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 {
- return strings.TrimSpace(matches[1]), true
- }
- }
- }
- }
- return "", false
- }
-
- func typeName(comments *ast.CommentGroup) (string, bool) {
-
- var typ string
- if comments != nil {
- for _, cmt := range comments.List {
- for _, ln := range strings.Split(cmt.Text, "\n") {
- matches := rxType.FindStringSubmatch(ln)
- if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 {
- typ = strings.TrimSpace(matches[1])
- return typ, true
- }
- }
- }
- }
- return "", false
- }
-
- func parseProperty(scp *schemaParser, gofile *ast.File, fld ast.Expr, prop swaggerTypable) error {
- switch ftpe := fld.(type) {
- case *ast.Ident: // simple value
- pkg, err := scp.packageForFile(gofile, ftpe)
- if err != nil {
- return err
- }
- return scp.parseIdentProperty(pkg, ftpe, prop)
-
- case *ast.StarExpr: // pointer to something, optional by default
- if err := parseProperty(scp, gofile, ftpe.X, prop); err != nil {
- return err
- }
-
- case *ast.ArrayType: // slice type
- if err := parseProperty(scp, gofile, ftpe.Elt, prop.Items()); err != nil {
- return err
- }
-
- case *ast.StructType:
- schema := prop.Schema()
- if schema == nil {
- return fmt.Errorf("items doesn't support embedded structs")
- }
- return scp.parseStructType(gofile, prop.Schema(), ftpe, make(map[string]string))
-
- case *ast.SelectorExpr:
- err := scp.typeForSelector(gofile, ftpe, prop)
- return err
-
- case *ast.MapType:
- // check if key is a string type, if not print a message
- // and skip the map property. Only maps with string keys can go into additional properties
- sch := prop.Schema()
- if sch == nil {
- return fmt.Errorf("items doesn't support maps")
- }
- if keyIdent, ok := ftpe.Key.(*ast.Ident); sch != nil && ok {
- if keyIdent.Name == "string" {
- if sch.AdditionalProperties == nil {
- sch.AdditionalProperties = new(spec.SchemaOrBool)
- }
- sch.AdditionalProperties.Allows = false
- if sch.AdditionalProperties.Schema == nil {
- sch.AdditionalProperties.Schema = new(spec.Schema)
- }
- if err := parseProperty(scp, gofile, ftpe.Value, schemaTypable{sch.AdditionalProperties.Schema, 0}); err != nil {
- return err
- }
- sch.Typed("object", "")
- }
- }
-
- case *ast.InterfaceType:
- prop.Schema().Typed("object", "")
- default:
- pos := "unknown file:unknown position"
- if scp != nil {
- if scp.program != nil {
- if scp.program.Fset != nil {
- pos = scp.program.Fset.Position(fld.Pos()).String()
- }
- }
- }
- return fmt.Errorf("Expr (%s) is unsupported for a schema", pos)
- }
- return nil
- }
-
- func parseJSONTag(field *ast.Field) (name string, ignore bool, isString bool, err error) {
- if len(field.Names) > 0 {
- name = field.Names[0].Name
- }
- if field.Tag != nil && len(strings.TrimSpace(field.Tag.Value)) > 0 {
- tv, err := strconv.Unquote(field.Tag.Value)
- if err != nil {
- return name, false, false, err
- }
-
- if strings.TrimSpace(tv) != "" {
- st := reflect.StructTag(tv)
- jsonParts := strings.Split(st.Get("json"), ",")
- jsonName := jsonParts[0]
-
- if len(jsonParts) > 1 && jsonParts[1] == "string" {
- isString = isFieldStringable(field.Type)
- }
-
- if jsonName == "-" {
- return name, true, isString, nil
- } else if jsonName != "" {
- return jsonName, false, isString, nil
- }
- }
- }
- return name, false, false, nil
- }
-
- // isFieldStringable check if the field type is a scalar. If the field type is
- // *ast.StarExpr and is pointer type, check if it refers to a scalar.
- // Otherwise, the ",string" directive doesn't apply.
- func isFieldStringable(tpe ast.Expr) bool {
- if ident, ok := tpe.(*ast.Ident); ok {
- switch ident.Name {
- case "int", "int8", "int16", "int32", "int64",
- "uint", "uint8", "uint16", "uint32", "uint64",
- "float64", "string", "bool":
- return true
- }
- } else if starExpr, ok := tpe.(*ast.StarExpr); ok {
- return isFieldStringable(starExpr.X)
- } else {
- return false
- }
- return false
- }
|