diff options
Diffstat (limited to 'vendor/github.com/go-swagger/go-swagger/codescan/schema.go')
| -rw-r--r-- | vendor/github.com/go-swagger/go-swagger/codescan/schema.go | 1092 |
1 files changed, 802 insertions, 290 deletions
diff --git a/vendor/github.com/go-swagger/go-swagger/codescan/schema.go b/vendor/github.com/go-swagger/go-swagger/codescan/schema.go index 640ac0830..670b2ece1 100644 --- a/vendor/github.com/go-swagger/go-swagger/codescan/schema.go +++ b/vendor/github.com/go-swagger/go-swagger/codescan/schema.go @@ -31,6 +31,8 @@ type schemaTypable struct { level int } +func (st schemaTypable) In() string { return "body" } + func (st schemaTypable) Typed(tpe, format string) { st.schema.Typed(tpe, format) } @@ -170,7 +172,7 @@ func (s *schemaBuilder) buildFromDecl(_ *entityDecl, schema *spec.Schema) error sp.setTitle = func(lines []string) { schema.Title = joinDropLast(lines) } sp.setDescription = func(lines []string) { schema.Description = joinDropLast(lines) - enumDesc := getEnumDesc(schema.VendorExtensible.Extensions) + enumDesc := getEnumDesc(schema.Extensions) if enumDesc != "" { schema.Description += "\n" + enumDesc } @@ -184,60 +186,83 @@ func (s *schemaBuilder) buildFromDecl(_ *entityDecl, schema *spec.Schema) error return nil } - switch tpe := s.decl.Type.Obj().Type().(type) { + defer func() { + if schema.Ref.String() == "" { + // unless this is a $ref, we add traceability of the origin of this schema in source + if s.Name != s.GoName { + addExtension(&schema.VendorExtensible, "x-go-name", s.GoName) + } + addExtension(&schema.VendorExtensible, "x-go-package", s.decl.Obj().Pkg().Path()) + } + }() + + switch tpe := s.decl.ObjType().(type) { + // TODO(fredbi): we may safely remove all the cases here that are not Named or Alias case *types.Basic: debugLog("basic: %v", tpe.Name()) + return nil case *types.Struct: - if err := s.buildFromStruct(s.decl, tpe, schema, make(map[string]string)); err != nil { - return err - } + return s.buildFromStruct(s.decl, tpe, schema, make(map[string]string)) case *types.Interface: - if err := s.buildFromInterface(s.decl, tpe, schema, make(map[string]string)); err != nil { - return err - } + return s.buildFromInterface(s.decl, tpe, schema, make(map[string]string)) case *types.Array: debugLog("array: %v -> %v", s.decl.Ident.Name, tpe.Elem().String()) + return nil case *types.Slice: debugLog("slice: %v -> %v", s.decl.Ident.Name, tpe.Elem().String()) + return nil case *types.Map: debugLog("map: %v -> [%v]%v", s.decl.Ident.Name, tpe.Key().String(), tpe.Elem().String()) + return nil case *types.Named: - o := tpe.Obj() - if o != nil { - debugLog("got the named type object: %s.%s | isAlias: %t | exported: %t", o.Pkg().Path(), o.Name(), o.IsAlias(), o.Exported()) - if o.Pkg().Name() == "time" && o.Name() == "Time" { - schema.Typed("string", "date-time") - return nil - } + debugLog("named: %v", tpe) + return s.buildDeclNamed(tpe, schema) + case *types.Alias: + debugLog("alias: %v -> %v", tpe, tpe.Rhs()) + tgt := schemaTypable{schema, 0} - ps := schemaTypable{schema, 0} - for { - ti := s.decl.Pkg.TypesInfo.Types[s.decl.Spec.Type] - if ti.IsBuiltin() { - break - } - if ti.IsType() { - if err := s.buildFromType(ti.Type, ps); err != nil { - return err - } - break - } - } - } + return s.buildDeclAlias(tpe, tgt) + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", tpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", tpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", tpe) + return nil default: - log.Printf("WARNING: Missing parser for a %T, skipping model: %s\n", tpe, s.Name) + log.Printf("WARNING: missing parser for type %T, skipping model: %s\n", tpe, s.Name) return nil } +} - if schema.Ref.String() == "" { - if s.Name != s.GoName { - addExtension(&schema.VendorExtensible, "x-go-name", s.GoName) - } - addExtension(&schema.VendorExtensible, "x-go-package", s.decl.Type.Obj().Pkg().Path()) +func (s *schemaBuilder) buildDeclNamed(tpe *types.Named, schema *spec.Schema) error { + if unsupportedBuiltin(tpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", tpe) + + return nil } - return nil + o := tpe.Obj() + + mustNotBeABuiltinType(o) + + debugLog("got the named type object: %s.%s | isAlias: %t | exported: %t", o.Pkg().Path(), o.Name(), o.IsAlias(), o.Exported()) + if isStdTime(o) { + schema.Typed("string", "date-time") + return nil + } + + ps := schemaTypable{schema, 0} + ti := s.decl.Pkg.TypesInfo.Types[s.decl.Spec.Type] + if !ti.IsType() { + return fmt.Errorf("declaration is not a type: %v", o) + } + + return s.buildFromType(ti.Type, ps) } +// buildFromTextMarshal renders a type that marshals as text as a string func (s *schemaBuilder) buildFromTextMarshal(tpe types.Type, tgt swaggerTypable) error { if typePtr, ok := tpe.(*types.Pointer); ok { return s.buildFromTextMarshal(typePtr.Elem(), tgt) @@ -250,7 +275,8 @@ func (s *schemaBuilder) buildFromTextMarshal(tpe types.Type, tgt swaggerTypable) } tio := typeNamed.Obj() - if tio.Pkg() == nil && tio.Name() == "error" { + if isStdError(tio) { + tgt.AddExtension("x-go-type", tio.Name()) return swaggerSchemaForType(tio.Name(), tgt) } @@ -264,17 +290,20 @@ func (s *schemaBuilder) buildFromTextMarshal(tpe types.Type, tgt swaggerTypable) if !found { // this must be a builtin - debugLog("skipping because package is nil: %s", tpe.String()) + debugLog("skipping because package is nil: %v", tpe) return nil } - if pkg.Name == "time" && tio.Name() == "Time" { + + if isStdTime(tio) { tgt.Typed("string", "date-time") return nil } - if pkg.PkgPath == "encoding/json" && tio.Name() == "RawMessage" { - tgt.Typed("object", "") + + if isStdJSONRawMessage(tio) { + tgt.Typed("object", "") // TODO: this should actually be any type return nil } + cmt, hasComments := s.ctx.FindComments(pkg, tio.Name()) if !hasComments { cmt = new(ast.CommentGroup) @@ -286,26 +315,26 @@ func (s *schemaBuilder) buildFromTextMarshal(tpe types.Type, tgt swaggerTypable) } tgt.Typed("string", "") + tgt.AddExtension("x-go-type", tio.Pkg().Path()+"."+tio.Name()) + return nil } func (s *schemaBuilder) buildFromType(tpe types.Type, tgt swaggerTypable) error { - pkg, err := importer.Default().Import("encoding") - if err != nil { - return nil - } - ifc := pkg.Scope().Lookup("TextMarshaler").Type().Underlying().(*types.Interface) - // check if the type implements encoding.TextMarshaler interface - isTextMarshaler := types.Implements(tpe, ifc) - if isTextMarshaler { + // if so, the type is rendered as a string. + debugLog("schema buildFromType %v (%T)", tpe, tpe) + + if isTextMarshaler(tpe) { return s.buildFromTextMarshal(tpe, tgt) } switch titpe := tpe.(type) { - case *types.Alias: - return nil // resolves panic case *types.Basic: + if unsupportedBuiltinType(titpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", tpe) + return nil + } return swaggerSchemaForType(titpe.String(), tgt) case *types.Pointer: return s.buildFromType(titpe.Elem(), tgt) @@ -314,167 +343,451 @@ func (s *schemaBuilder) buildFromType(tpe types.Type, tgt swaggerTypable) error case *types.Interface: return s.buildFromInterface(s.decl, titpe, tgt.Schema(), make(map[string]string)) case *types.Slice: + // anonymous slice return s.buildFromType(titpe.Elem(), tgt.Items()) case *types.Array: + // anonymous array return s.buildFromType(titpe.Elem(), tgt.Items()) case *types.Map: - // debugLog("map: %v -> [%v]%v", fld.Name(), ftpe.Key().String(), ftpe.Elem().String()) - // 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 := tgt.Schema() - if sch == nil { - return errors.New("items doesn't support maps") - } - eleProp := schemaTypable{sch, tgt.Level()} - key := titpe.Key() - isTextMarshaler := types.Implements(key, ifc) - if key.Underlying().String() == "string" || isTextMarshaler { - return s.buildFromType(titpe.Elem(), eleProp.AdditionalProperties()) - } + return s.buildFromMap(titpe, tgt) case *types.Named: - tio := titpe.Obj() - if tio.Pkg() == nil && tio.Name() == "error" { - return swaggerSchemaForType(tio.Name(), tgt) - } - debugLog("named refined type %s.%s", tio.Pkg().Path(), tio.Name()) - pkg, found := s.ctx.PkgForType(tpe) - if !found { - // this must be a builtin - debugLog("skipping because package is nil: %s", tpe.String()) + // a named type, e.g. type X struct {} + return s.buildNamedType(titpe, tgt) + case *types.Alias: + // a named alias, e.g. type X = {RHS type}. + debugLog("alias(schema.buildFromType): got alias %v to %v", titpe, titpe.Rhs()) + return s.buildAlias(titpe, tgt) + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", titpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", tpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", tpe) + return nil + default: + panic(fmt.Errorf("ERROR: can't determine refined type %[1]v (%[1]T): %w", titpe, ErrInternal)) + } +} + +func (s *schemaBuilder) buildNamedType(titpe *types.Named, tgt swaggerTypable) error { + tio := titpe.Obj() + if unsupportedBuiltin(titpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", titpe) + return nil + } + if isAny(tio) { + // e.g type X any or type X interface{} + _ = tgt.Schema() + + return nil + } + + // special case of the "error" interface. + if isStdError(tio) { + tgt.AddExtension("x-go-type", tio.Name()) + return swaggerSchemaForType(tio.Name(), tgt) + } + + // special case of the "time.Time" type + if isStdTime(tio) { + tgt.Typed("string", "date-time") + return nil + } + + // special case of the "json.RawMessage" type + if isStdJSONRawMessage(tio) { + tgt.Typed("object", "") // TODO: this should actually be any type + return nil + } + + pkg, found := s.ctx.PkgForType(titpe) + debugLog("named refined type %s.%s", pkg, tio.Name()) + if !found { + // this must be a builtin + // + // This could happen for example when using unsupported types such as complex64, complex128, uintptr, + // or type constraints such as comparable. + debugLog("skipping because package is nil (builtin type): %v", tio) + + return nil + } + + cmt, hasComments := s.ctx.FindComments(pkg, tio.Name()) + if !hasComments { + cmt = new(ast.CommentGroup) + } + + if typeName, ok := typeName(cmt); ok { + _ = swaggerSchemaForType(typeName, tgt) + + return nil + } + + if s.decl.Spec.Assign.IsValid() { + debugLog("found assignment: %s.%s", tio.Pkg().Path(), tio.Name()) + return s.buildFromType(titpe.Underlying(), tgt) + } + + if titpe.TypeArgs() != nil && titpe.TypeArgs().Len() > 0 { + return s.buildFromType(titpe.Underlying(), tgt) + } + + // invariant: the Underlying cannot be an alias or named type + switch utitpe := titpe.Underlying().(type) { + case *types.Struct: + debugLog("found struct: %s.%s", tio.Pkg().Path(), tio.Name()) + + decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()) + if !ok { + debugLog("could not find model in index: %s.%s", tio.Pkg().Path(), tio.Name()) return nil } - if pkg.Name == "time" && tio.Name() == "Time" { + + o := decl.Obj() + if isStdTime(o) { tgt.Typed("string", "date-time") return nil } - if pkg.PkgPath == "encoding/json" && tio.Name() == "RawMessage" { - tgt.Typed("object", "") + + if sfnm, isf := strfmtName(cmt); isf { + tgt.Typed("string", sfnm) return nil } - cmt, hasComments := s.ctx.FindComments(pkg, tio.Name()) - if !hasComments { - cmt = new(ast.CommentGroup) - } if typeName, ok := typeName(cmt); ok { _ = swaggerSchemaForType(typeName, tgt) return nil } - if s.decl.Spec.Assign.IsValid() { - return s.buildFromType(titpe.Underlying(), tgt) + return s.makeRef(decl, tgt) + case *types.Interface: + debugLog("found interface: %s.%s", tio.Pkg().Path(), tio.Name()) + + decl, found := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()) + if !found { + return fmt.Errorf("can't find source file for type: %v", utitpe) } - if titpe.TypeArgs() != nil && titpe.TypeArgs().Len() > 0 { - return s.buildFromType(titpe.Underlying(), tgt) + return s.makeRef(decl, tgt) + case *types.Basic: + if unsupportedBuiltinType(utitpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", utitpe) + return nil } - switch utitpe := tpe.Underlying().(type) { - case *types.Struct: - if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { - if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" { - tgt.Typed("string", "date-time") - return nil - } - if sfnm, isf := strfmtName(cmt); isf { - tgt.Typed("string", sfnm) - return nil - } - if typeName, ok := typeName(cmt); ok { - _ = swaggerSchemaForType(typeName, tgt) - return nil - } + debugLog("found primitive type: %s.%s", tio.Pkg().Path(), tio.Name()) - return s.makeRef(decl, tgt) - } - case *types.Interface: - if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { - return s.makeRef(decl, tgt) + if sfnm, isf := strfmtName(cmt); isf { + tgt.Typed("string", sfnm) + return nil + } + + if enumName, ok := enumName(cmt); ok { + enumValues, enumDesces, _ := s.ctx.FindEnumValues(pkg, enumName) + if len(enumValues) > 0 { + tgt.WithEnum(enumValues...) + enumTypeName := reflect.TypeOf(enumValues[0]).String() + _ = swaggerSchemaForType(enumTypeName, tgt) } - case *types.Basic: - if sfnm, isf := strfmtName(cmt); isf { - tgt.Typed("string", sfnm) - return nil + if len(enumDesces) > 0 { + tgt.WithEnumDescription(strings.Join(enumDesces, "\n")) } + return nil + } - if enumName, ok := enumName(cmt); ok { - enumValues, enumDesces, _ := s.ctx.FindEnumValues(pkg, enumName) - if len(enumValues) > 0 { - tgt.WithEnum(enumValues...) - enumTypeName := reflect.TypeOf(enumValues[0]).String() - _ = swaggerSchemaForType(enumTypeName, tgt) - } - if len(enumDesces) > 0 { - tgt.WithEnumDescription(strings.Join(enumDesces, "\n")) - } - return nil - } + if defaultName, ok := defaultName(cmt); ok { + debugLog("default name: %s", defaultName) + return nil + } - if defaultName, ok := defaultName(cmt); ok { - debugLog(defaultName) //nolint:govet + if typeName, ok := typeName(cmt); ok { + _ = swaggerSchemaForType(typeName, tgt) + return nil + + } + + if isAliasParam(tgt) || aliasParam(cmt) { + err := swaggerSchemaForType(utitpe.Name(), tgt) + if err == nil { return nil } + } - if typeName, ok := typeName(cmt); ok { - _ = swaggerSchemaForType(typeName, tgt) - return nil + if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { + return s.makeRef(decl, tgt) + } - } + return swaggerSchemaForType(utitpe.String(), tgt) + case *types.Array: + debugLog("found array type: %s.%s", tio.Pkg().Path(), tio.Name()) - if isAliasParam(tgt) || aliasParam(cmt) { - err := swaggerSchemaForType(utitpe.Name(), tgt) - if err == nil { - return nil - } + if sfnm, isf := strfmtName(cmt); isf { + if sfnm == "byte" { + tgt.Typed("string", sfnm) + return nil } - if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { - return s.makeRef(decl, tgt) + if sfnm == "bsonobjectid" { + tgt.Typed("string", sfnm) + return nil } - return swaggerSchemaForType(utitpe.String(), tgt) - case *types.Array: - if sfnm, isf := strfmtName(cmt); isf { - if sfnm == "byte" { - tgt.Typed("string", sfnm) - return nil - } - if sfnm == "bsonobjectid" { - tgt.Typed("string", sfnm) - return nil - } - tgt.Items().Typed("string", sfnm) + tgt.Items().Typed("string", sfnm) + return nil + } + if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { + return s.makeRef(decl, tgt) + } + return s.buildFromType(utitpe.Elem(), tgt.Items()) + case *types.Slice: + debugLog("found slice type: %s.%s", tio.Pkg().Path(), tio.Name()) + + if sfnm, isf := strfmtName(cmt); isf { + if sfnm == "byte" { + tgt.Typed("string", sfnm) return nil } - if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { - return s.makeRef(decl, tgt) + tgt.Items().Typed("string", sfnm) + return nil + } + if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { + return s.makeRef(decl, tgt) + } + return s.buildFromType(utitpe.Elem(), tgt.Items()) + case *types.Map: + debugLog("found map type: %s.%s", tio.Pkg().Path(), tio.Name()) + + if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { + return s.makeRef(decl, tgt) + } + return nil + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", utitpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", utitpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", utitpe) + return nil + default: + log.Printf( + "WARNING: can't figure out object type for named type (%T): %v [alias: %t]", + titpe.Underlying(), titpe.Underlying(), titpe.Obj().IsAlias(), + ) + + return nil + } +} + +// buildDeclAlias builds a top-level alias declaration. +func (s *schemaBuilder) buildDeclAlias(tpe *types.Alias, tgt swaggerTypable) error { + if unsupportedBuiltinType(tpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", tpe) + return nil + } + + o := tpe.Obj() + if isAny(o) { + _ = tgt.Schema() // this is mutating tgt to create an empty schema + return nil + } + if isStdError(o) { + tgt.AddExtension("x-go-type", o.Name()) + return swaggerSchemaForType(o.Name(), tgt) + } + mustNotBeABuiltinType(o) + + if isStdTime(o) { + tgt.Typed("string", "date-time") + return nil + } + + mustHaveRightHandSide(tpe) + rhs := tpe.Rhs() + + decl, ok := s.ctx.FindModel(o.Pkg().Path(), o.Name()) + if !ok { + return fmt.Errorf("can't find source file for aliased type: %v -> %v", tpe, rhs) + } + + s.postDecls = append(s.postDecls, decl) // mark the left-hand side as discovered + + if !s.ctx.app.refAliases { + // expand alias + return s.buildFromType(tpe.Underlying(), tgt) + } + + // resolve alias to named type as $ref + switch rtpe := rhs.(type) { + // named declarations: we construct a $ref to the right-hand side target of the alias + case *types.Named: + ro := rtpe.Obj() + rdecl, found := s.ctx.FindModel(ro.Pkg().Path(), ro.Name()) + if !found { + return fmt.Errorf("can't find source file for target type of alias: %v -> %v", tpe, rtpe) + } + + return s.makeRef(rdecl, tgt) + case *types.Alias: + ro := rtpe.Obj() + if unsupportedBuiltin(rtpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", rtpe) + return nil + } + if isAny(ro) { + // e.g. type X = any + _ = tgt.Schema() // this is mutating tgt to create an empty schema + return nil + } + if isStdError(ro) { + // e.g. type X = error + tgt.AddExtension("x-go-type", o.Name()) + return swaggerSchemaForType(o.Name(), tgt) + } + mustNotBeABuiltinType(ro) // TODO(fred): there are a few other cases + + rdecl, found := s.ctx.FindModel(ro.Pkg().Path(), ro.Name()) + if !found { + return fmt.Errorf("can't find source file for target type of alias: %v -> %v", tpe, rtpe) + } + + return s.makeRef(rdecl, tgt) + } + + // alias to anonymous type + return s.buildFromType(rhs, tgt) +} + +func (s *schemaBuilder) buildAnonymousInterface(it *types.Interface, tgt swaggerTypable, decl *entityDecl) error { + tgt.Typed("object", "") + + for i := range it.NumExplicitMethods() { + fld := it.ExplicitMethod(i) + if !fld.Exported() { + continue + } + sig, isSignature := fld.Type().(*types.Signature) + if !isSignature { + continue + } + if sig.Params().Len() > 0 { + continue + } + if sig.Results() == nil || sig.Results().Len() != 1 { + continue + } + + var afld *ast.Field + ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos()) + // debugLog("got %d nodes (exact: %t)", len(ans), isExact) + for _, an := range ans { + at, valid := an.(*ast.Field) + if !valid { + continue } - return s.buildFromType(utitpe.Elem(), tgt.Items()) - case *types.Slice: - if sfnm, isf := strfmtName(cmt); isf { - if sfnm == "byte" { - tgt.Typed("string", sfnm) - return nil + + debugLog("maybe interface field %s: %s(%T)", fld.Name(), fld.Type().String(), fld.Type()) + afld = at + break + } + + if afld == nil { + debugLog("can't find source associated with %s for %s", fld.String(), it.String()) + continue + } + + // if the field is annotated with swagger:ignore, ignore it + if ignored(afld.Doc) { + continue + } + + name := fld.Name() + if afld.Doc != nil { + for _, cmt := range afld.Doc.List { + for _, ln := range strings.Split(cmt.Text, "\n") { + matches := rxName.FindStringSubmatch(ln) + ml := len(matches) + if ml > 1 { + name = matches[ml-1] + } } - tgt.Items().Typed("string", sfnm) - return nil - } - if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { - return s.makeRef(decl, tgt) - } - return s.buildFromType(utitpe.Elem(), tgt.Items()) - case *types.Map: - if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { - return s.makeRef(decl, tgt) } - return nil + } - default: - log.Printf("WARNING: can't figure out object type for named type (%T): %v [alias: %t]", tpe.Underlying(), tpe.Underlying(), titpe.Obj().IsAlias()) + if tgt.Schema().Properties == nil { + tgt.Schema().Properties = make(map[string]spec.Schema) + } + ps := tgt.Schema().Properties[name] + if err := s.buildFromType(sig.Results().At(0).Type(), schemaTypable{&ps, 0}); err != nil { + return err + } + if sfName, isStrfmt := strfmtName(afld.Doc); isStrfmt { + ps.Typed("string", sfName) + ps.Ref = spec.Ref{} + ps.Items = nil + } - return nil + if err := s.createParser(name, tgt.Schema(), &ps, afld).Parse(afld.Doc); err != nil { + return err } - default: - panic(fmt.Sprintf("WARNING: can't determine refined type %s (%T)", titpe.String(), titpe)) + + if ps.Ref.String() == "" && name != fld.Name() { + ps.AddExtension("x-go-name", fld.Name()) + } + + if s.ctx.app.setXNullableForPointers { + if _, isPointer := fld.Type().(*types.Signature).Results().At(0).Type().(*types.Pointer); isPointer && (ps.Extensions == nil || (ps.Extensions["x-nullable"] == nil && ps.Extensions["x-isnullable"] == nil)) { + ps.AddExtension("x-nullable", true) + } + } + + // seen[name] = fld.Name() + tgt.Schema().Properties[name] = ps + } + + return nil +} + +// buildAlias builds a reference to an alias from another type. +func (s *schemaBuilder) buildAlias(tpe *types.Alias, tgt swaggerTypable) error { + if unsupportedBuiltinType(tpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", tpe) + + return nil + } + + o := tpe.Obj() + if isAny(o) { + _ = tgt.Schema() + return nil + } + mustNotBeABuiltinType(o) + + decl, ok := s.ctx.FindModel(o.Pkg().Path(), o.Name()) + if !ok { + return fmt.Errorf("can't find source file for aliased type: %v", tpe) + } + + return s.makeRef(decl, tgt) +} + +func (s *schemaBuilder) buildFromMap(titpe *types.Map, tgt swaggerTypable) error { + // check if key is a string type, or knows how to marshall to text. + // If not, print a message and skip the map property. + // + // Only maps with string keys can go into additional properties + + sch := tgt.Schema() + if sch == nil { + return errors.New("items doesn't support maps") + } + + eleProp := schemaTypable{sch, tgt.Level()} + key := titpe.Key() + if key.Underlying().String() == "string" || isTextMarshaler(key) { + return s.buildFromType(titpe.Elem(), eleProp.AdditionalProperties()) } return nil @@ -482,6 +795,7 @@ func (s *schemaBuilder) buildFromType(tpe types.Type, tgt swaggerTypable) error func (s *schemaBuilder) buildFromInterface(decl *entityDecl, it *types.Interface, schema *spec.Schema, seen map[string]string) error { if it.Empty() { + // return an empty schema for empty interfaces return nil } @@ -494,98 +808,91 @@ func (s *schemaBuilder) buildFromInterface(decl *entityDecl, it *types.Interface if specType, ok := decl.Spec.Type.(*ast.InterfaceType); ok { flist = make([]*ast.Field, it.NumEmbeddeds()+it.NumExplicitMethods()) copy(flist, specType.Methods.List) - // for i := range specType.Methods.List { - // flist[i] = specType.Methods.List[i] - // } } // First collect the embedded interfaces - // create refs when the embedded interface is decorated with an allOf annotation - for i := 0; i < it.NumEmbeddeds(); i++ { + // create refs when: + // + // 1. the embedded interface is decorated with an allOf annotation + // 2. the embedded interface is an alias + for i := range it.NumEmbeddeds() { fld := it.EmbeddedType(i) + debugLog("inspecting embedded type in interface: %v", fld) + var ( + fieldHasAllOf bool + err error + ) + + if tgt == nil { + tgt = &spec.Schema{} + } switch ftpe := fld.(type) { case *types.Named: + debugLog("embedded named type (buildInterface): %v", ftpe) o := ftpe.Obj() - var afld *ast.Field - for _, an := range flist { - if len(an.Names) != 0 { - continue - } - - tpp := decl.Pkg.TypesInfo.Types[an.Type] - if tpp.Type.String() != o.Type().String() { - continue - } - - // decl. - debugLog("maybe interface field %s: %s(%T)", o.Name(), o.Type().String(), o.Type()) - afld = an - break - } - - if afld == nil { - debugLog("can't find source associated with %s for %s", fld.String(), it.String()) + if isAny(o) || isStdError(o) { + // ignore bultin interfaces continue } - // if the field is annotated with swagger:ignore, ignore it - if ignored(afld.Doc) { - continue + if fieldHasAllOf, err = s.buildNamedInterface(ftpe, flist, decl, schema, seen); err != nil { + return err } - - if !allOfMember(afld.Doc) { - var newSch spec.Schema - if err := s.buildEmbedded(o.Type(), &newSch, seen); err != nil { - return err - } - schema.AllOf = append(schema.AllOf, newSch) - hasAllOf = true - continue + case *types.Interface: + debugLog("embedded anonymous interface type (buildInterface): %v", ftpe) // e.g. type X interface{ interface{Error() string}} + var aliasedSchema spec.Schema + ps := schemaTypable{schema: &aliasedSchema} + if err = s.buildAnonymousInterface(ftpe, ps, decl); err != nil { + return err } - hasAllOf = true - if tgt == nil { - tgt = &spec.Schema{} + if aliasedSchema.Ref.String() != "" || len(aliasedSchema.Properties) > 0 || len(aliasedSchema.AllOf) > 0 { + schema.AddToAllOf(aliasedSchema) + fieldHasAllOf = true } - 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 := s.buildAllOf(o.Type(), &newSch); err != nil { + case *types.Alias: + debugLog("embedded alias (buildInterface): %v -> %v", ftpe, ftpe.Rhs()) + var aliasedSchema spec.Schema + ps := schemaTypable{schema: &aliasedSchema} + if err = s.buildAlias(ftpe, ps); err != nil { return err } - if afld.Doc != nil { - for _, cmt := range afld.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 != "" { - schema.AddExtension("x-class", mv) - } - } - } - } - } - schema.AllOf = append(schema.AllOf, newSch) + if aliasedSchema.Ref.String() != "" || len(aliasedSchema.Properties) > 0 || len(aliasedSchema.AllOf) > 0 { + schema.AddToAllOf(aliasedSchema) + fieldHasAllOf = true + } + case *types.Union: // e.g. type X interface{ ~uint16 | ~float32 } + log.Printf("WARNING: union type constraints are not supported yet %[1]v (%[1]T). Skipped", ftpe) + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", ftpe) + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", ftpe) + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", ftpe) default: - log.Printf("WARNING: can't figure out object type for allOf named type (%T): %v", ftpe, ftpe.Underlying()) + log.Printf( + "WARNING: can't figure out object type for allOf named type (%T): %v", + ftpe, ftpe.Underlying(), + ) } - debugLog("got embedded interface: %s {%T}", fld.String(), fld) + + debugLog("got embedded interface: %v {%T}, fieldHasAllOf: %t", fld, fld, fieldHasAllOf) + hasAllOf = hasAllOf || fieldHasAllOf } if tgt == nil { tgt = schema } + // We can finally build the actual schema for the struct if tgt.Properties == nil { tgt.Properties = make(map[string]spec.Schema) } tgt.Typed("object", "") - for i := 0; i < it.NumExplicitMethods(); i++ { + for i := range it.NumExplicitMethods() { fld := it.ExplicitMethod(i) if !fld.Exported() { continue @@ -671,17 +978,91 @@ func (s *schemaBuilder) buildFromInterface(decl *entityDecl, it *types.Interface if hasAllOf && len(tgt.Properties) > 0 { schema.AllOf = append(schema.AllOf, *tgt) } + for k := range tgt.Properties { if _, ok := seen[k]; !ok { delete(tgt.Properties, k) } } + return nil } +func (s *schemaBuilder) buildNamedInterface(ftpe *types.Named, flist []*ast.Field, decl *entityDecl, schema *spec.Schema, seen map[string]string) (hasAllOf bool, err error) { + o := ftpe.Obj() + var afld *ast.Field + + for _, an := range flist { + if len(an.Names) != 0 { + continue + } + + tpp := decl.Pkg.TypesInfo.Types[an.Type] + if tpp.Type.String() != o.Type().String() { + continue + } + + // decl. + debugLog("maybe interface field %s: %s(%T)", o.Name(), o.Type().String(), o.Type()) + afld = an + break + } + + if afld == nil { + debugLog("can't find source associated with %s", ftpe.String()) + return hasAllOf, nil + } + + // if the field is annotated with swagger:ignore, ignore it + if ignored(afld.Doc) { + return hasAllOf, nil + } + + if !allOfMember(afld.Doc) { + var newSch spec.Schema + if err = s.buildEmbedded(o.Type(), &newSch, seen); err != nil { + return hasAllOf, err + } + schema.AllOf = append(schema.AllOf, newSch) + hasAllOf = true + + return hasAllOf, nil + } + + hasAllOf = true + + 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 = s.buildAllOf(o.Type(), &newSch); err != nil { + return hasAllOf, err + } + + if afld.Doc != nil { + for _, cmt := range afld.Doc.List { + for _, ln := range strings.Split(cmt.Text, "\n") { + matches := rxAllOf.FindStringSubmatch(ln) + ml := len(matches) + if ml <= 1 { + continue + } + + mv := matches[ml-1] + if mv != "" { + schema.AddExtension("x-class", mv) + } + } + } + } + + schema.AllOf = append(schema.AllOf, newSch) + + return hasAllOf, nil +} + func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, schema *spec.Schema, seen map[string]string) error { - s.ctx.FindComments(decl.Pkg, decl.Type.Obj().Name()) - cmt, hasComments := s.ctx.FindComments(decl.Pkg, decl.Type.Obj().Name()) + s.ctx.FindComments(decl.Pkg, decl.Obj().Name()) + cmt, hasComments := s.ctx.FindComments(decl.Pkg, decl.Obj().Name()) if !hasComments { cmt = new(ast.CommentGroup) } @@ -693,15 +1074,19 @@ func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, sche var tgt *spec.Schema hasAllOf := false - for i := 0; i < st.NumFields(); i++ { + for i := range st.NumFields() { fld := st.Field(i) if !fld.Anonymous() { + // e.g. struct { _ struct{} } debugLog("skipping field %q for allOf scan because not anonymous", fld.Name()) continue } tg := st.Tag(i) - debugLog("maybe allof field(%t) %s: %s (%T) [%q](anon: %t, embedded: %t)", fld.IsField(), fld.Name(), fld.Type().String(), fld.Type(), tg, fld.Anonymous(), fld.Embedded()) + debugLog( + "maybe allof field(%t) %s: %s (%T) [%q](anon: %t, embedded: %t)", + fld.IsField(), fld.Name(), fld.Type().String(), fld.Type(), tg, fld.Anonymous(), fld.Embedded(), + ) var afld *ast.Field ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos()) // debugLog("got %d nodes (exact: %t)", len(ans), isExact) @@ -734,7 +1119,9 @@ func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, sche continue } - if !allOfMember(afld.Doc) { + _, isAliased := fld.Type().(*types.Alias) + + if !allOfMember(afld.Doc) && !isAliased { if tgt == nil { tgt = schema } @@ -743,6 +1130,11 @@ func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, sche } continue } + + if isAliased { + debugLog("alias member in struct: %v", fld) + } + // 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 @@ -789,7 +1181,7 @@ func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, sche } tgt.Typed("object", "") - for i := 0; i < st.NumFields(); i++ { + for i := range st.NumFields() { fld := st.Field(i) tg := st.Tag(i) @@ -804,7 +1196,6 @@ func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, sche var afld *ast.Field ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos()) - // debugLog("got %d nodes (exact: %t)", len(ans), isExact) for _, an := range ans { at, valid := an.(*ast.Field) if !valid { @@ -894,79 +1285,174 @@ func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, sche func (s *schemaBuilder) buildAllOf(tpe types.Type, schema *spec.Schema) error { debugLog("allOf %s", tpe.Underlying()) + switch ftpe := tpe.(type) { case *types.Pointer: return s.buildAllOf(ftpe.Elem(), schema) case *types.Named: - switch utpe := ftpe.Underlying().(type) { - case *types.Struct: - decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) - if found { - if ftpe.Obj().Pkg().Path() == "time" && ftpe.Obj().Name() == "Time" { - schema.Typed("string", "date-time") - return nil - } - if sfnm, isf := strfmtName(decl.Comments); isf { - schema.Typed("string", sfnm) - return nil - } - if decl.HasModelAnnotation() { - return s.makeRef(decl, schemaTypable{schema, 0}) - } - return s.buildFromStruct(decl, utpe, schema, make(map[string]string)) - } + return s.buildNamedAllOf(ftpe, schema) + case *types.Alias: + debugLog("allOf member is alias %v => %v", ftpe, ftpe.Rhs()) + tgt := schemaTypable{schema: schema} + return s.buildAlias(ftpe, tgt) + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", ftpe) + return nil + default: + log.Printf("WARNING: missing allOf parser for a %T, skipping field", ftpe) + return fmt.Errorf("unable to resolve allOf member for: %v", ftpe) + } +} + +func (s *schemaBuilder) buildNamedAllOf(ftpe *types.Named, schema *spec.Schema) error { + switch utpe := ftpe.Underlying().(type) { + case *types.Struct: + decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) + if !found { return fmt.Errorf("can't find source file for struct: %s", ftpe.String()) - case *types.Interface: - decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) - if found { - if sfnm, isf := strfmtName(decl.Comments); isf { - schema.Typed("string", sfnm) - return nil - } - if decl.HasModelAnnotation() { - return s.makeRef(decl, schemaTypable{schema, 0}) - } - return s.buildFromInterface(decl, utpe, schema, make(map[string]string)) - } + } + + if isStdTime(ftpe.Obj()) { + schema.Typed("string", "date-time") + return nil + } + + if sfnm, isf := strfmtName(decl.Comments); isf { + schema.Typed("string", sfnm) + return nil + } + + if decl.HasModelAnnotation() { + return s.makeRef(decl, schemaTypable{schema, 0}) + } + + return s.buildFromStruct(decl, utpe, schema, make(map[string]string)) + case *types.Interface: + decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) + if !found { return fmt.Errorf("can't find source file for interface: %s", ftpe.String()) - default: - log.Printf("WARNING: can't figure out object type for allOf named type (%T): %v", ftpe, ftpe.Underlying()) - return fmt.Errorf("unable to locate source file for allOf %s", utpe.String()) } + + if sfnm, isf := strfmtName(decl.Comments); isf { + schema.Typed("string", sfnm) + return nil + } + + if decl.HasModelAnnotation() { + return s.makeRef(decl, schemaTypable{schema, 0}) + } + + return s.buildFromInterface(decl, utpe, schema, make(map[string]string)) + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", ftpe) + return nil default: - log.Printf("WARNING: Missing allOf parser for a %T, skipping field", ftpe) - return fmt.Errorf("unable to resolve allOf member for: %v", ftpe) + log.Printf( + "WARNING: can't figure out object type for allOf named type (%T): %v", + ftpe, utpe, + ) + return fmt.Errorf("unable to locate source file for allOf (%T): %v", + ftpe, utpe, + ) } } func (s *schemaBuilder) buildEmbedded(tpe types.Type, schema *spec.Schema, seen map[string]string) error { - debugLog("embedded %s", tpe.Underlying()) + debugLog("embedded %v", tpe.Underlying()) + switch ftpe := tpe.(type) { case *types.Pointer: return s.buildEmbedded(ftpe.Elem(), schema, seen) case *types.Named: - debugLog("embedded named type: %T", ftpe.Underlying()) - switch utpe := ftpe.Underlying().(type) { - case *types.Struct: - decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) - if found { - return s.buildFromStruct(decl, utpe, schema, seen) - } + return s.buildNamedEmbedded(ftpe, schema, seen) + case *types.Alias: + debugLog("embedded alias %v => %v", ftpe, ftpe.Rhs()) + tgt := schemaTypable{schema, 0} + return s.buildAlias(ftpe, tgt) + case *types.Union: // e.g. type X interface{ ~uint16 | ~float32 } + log.Printf("WARNING: union type constraints are not supported yet %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", ftpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", ftpe) + return nil + default: + log.Printf("WARNING: Missing embedded parser for a %T, skipping model\n", ftpe) + return nil + } +} + +func (s *schemaBuilder) buildNamedEmbedded(ftpe *types.Named, schema *spec.Schema, seen map[string]string) error { + debugLog("embedded named type: %T", ftpe.Underlying()) + if unsupportedBuiltin(ftpe) { + log.Printf("WARNING: skipped unsupported builtin type: %v", ftpe) + + return nil + } + + switch utpe := ftpe.Underlying().(type) { + case *types.Struct: + decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) + if !found { return fmt.Errorf("can't find source file for struct: %s", ftpe.String()) - case *types.Interface: - decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) - if found { - return s.buildFromInterface(decl, utpe, schema, seen) - } + } + + return s.buildFromStruct(decl, utpe, schema, seen) + case *types.Interface: + if utpe.Empty() { + return nil + } + o := ftpe.Obj() + if isAny(o) { + return nil + } + if isStdError(o) { + tgt := schemaTypable{schema: schema} + tgt.AddExtension("x-go-type", o.Name()) + return swaggerSchemaForType(o.Name(), tgt) + } + mustNotBeABuiltinType(o) + + decl, found := s.ctx.FindModel(o.Pkg().Path(), o.Name()) + if !found { return fmt.Errorf("can't find source file for struct: %s", ftpe.String()) - default: - log.Printf("WARNING: can't figure out object type for embedded named type (%T): %v", ftpe, ftpe.Underlying()) } + return s.buildFromInterface(decl, utpe, schema, seen) + case *types.Union: // e.g. type X interface{ ~uint16 | ~float32 } + log.Printf("WARNING: union type constraints are not supported yet %[1]v (%[1]T). Skipped", utpe) + return nil + case *types.TypeParam: + log.Printf("WARNING: generic type parameters are not supported yet %[1]v (%[1]T). Skipped", utpe) + return nil + case *types.Chan: + log.Printf("WARNING: channels are not supported %[1]v (%[1]T). Skipped", utpe) + return nil + case *types.Signature: + log.Printf("WARNING: functions are not supported %[1]v (%[1]T). Skipped", utpe) + return nil default: - log.Printf("WARNING: Missing embedded parser for a %T, skipping model\n", ftpe) + log.Printf("WARNING: can't figure out object type for embedded named type (%T): %v", + ftpe, utpe, + ) return nil } - return nil } func (s *schemaBuilder) makeRef(decl *entityDecl, prop swaggerTypable) error { @@ -991,7 +1477,7 @@ func (s *schemaBuilder) createParser(nm string, schema, ps *spec.Schema, fld *as if ps.Ref.String() == "" { sp.setDescription = func(lines []string) { ps.Description = joinDropLast(lines) - enumDesc := getEnumDesc(ps.VendorExtensible.Extensions) + enumDesc := getEnumDesc(ps.Extensions) if enumDesc != "" { ps.Description += "\n" + enumDesc } @@ -1070,7 +1556,7 @@ func (s *schemaBuilder) createParser(nm string, schema, ps *spec.Schema, fld *as } return otherTaggers, nil default: - return nil, fmt.Errorf("unknown field type ele for %q", nm) + return nil, fmt.Errorf("unknown field type element for %q", nm) } } // check if this is a primitive, if so parse the validations from the @@ -1178,3 +1664,29 @@ func isFieldStringable(tpe ast.Expr) bool { } return false } + +func isTextMarshaler(tpe types.Type) bool { + encodingPkg, err := importer.Default().Import("encoding") + if err != nil { + return false + } + ifc := encodingPkg.Scope().Lookup("TextMarshaler").Type().Underlying().(*types.Interface) // TODO: there is a better way to check this + + return types.Implements(tpe, ifc) +} + +func isStdTime(o *types.TypeName) bool { + return o.Pkg() != nil && o.Pkg().Name() == "time" && o.Name() == "Time" +} + +func isStdError(o *types.TypeName) bool { + return o.Pkg() == nil && o.Name() == "error" +} + +func isStdJSONRawMessage(o *types.TypeName) bool { + return o.Pkg() != nil && o.Pkg().Path() == "encoding/json" && o.Name() == "RawMessage" +} + +func isAny(o *types.TypeName) bool { + return o.Pkg() == nil && o.Name() == "any" +} |
