github.com/go-swagger/go-swagger@v0.31.0/codescan/schema.go (about) 1 package codescan 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "go/ast" 8 "go/importer" 9 "go/types" 10 "log" 11 "os" 12 "reflect" 13 "strconv" 14 "strings" 15 16 "golang.org/x/tools/go/ast/astutil" 17 18 "github.com/go-openapi/spec" 19 ) 20 21 func addExtension(ve *spec.VendorExtensible, key string, value interface{}) { 22 if os.Getenv("SWAGGER_GENERATE_EXTENSION") == "false" { 23 return 24 } 25 26 ve.AddExtension(key, value) 27 } 28 29 type schemaTypable struct { 30 schema *spec.Schema 31 level int 32 } 33 34 func (st schemaTypable) Typed(tpe, format string) { 35 st.schema.Typed(tpe, format) 36 } 37 38 func (st schemaTypable) SetRef(ref spec.Ref) { 39 st.schema.Ref = ref 40 } 41 42 func (st schemaTypable) Schema() *spec.Schema { 43 return st.schema 44 } 45 46 func (st schemaTypable) Items() swaggerTypable { 47 if st.schema.Items == nil { 48 st.schema.Items = new(spec.SchemaOrArray) 49 } 50 if st.schema.Items.Schema == nil { 51 st.schema.Items.Schema = new(spec.Schema) 52 } 53 54 st.schema.Typed("array", "") 55 return schemaTypable{st.schema.Items.Schema, st.level + 1} 56 } 57 58 func (st schemaTypable) AdditionalProperties() swaggerTypable { 59 if st.schema.AdditionalProperties == nil { 60 st.schema.AdditionalProperties = new(spec.SchemaOrBool) 61 } 62 if st.schema.AdditionalProperties.Schema == nil { 63 st.schema.AdditionalProperties.Schema = new(spec.Schema) 64 } 65 66 st.schema.Typed("object", "") 67 return schemaTypable{st.schema.AdditionalProperties.Schema, st.level + 1} 68 } 69 70 func (st schemaTypable) Level() int { return st.level } 71 72 func (st schemaTypable) AddExtension(key string, value interface{}) { 73 addExtension(&st.schema.VendorExtensible, key, value) 74 } 75 76 func (st schemaTypable) WithEnum(values ...interface{}) { 77 st.schema.WithEnum(values...) 78 } 79 80 func (st schemaTypable) WithEnumDescription(desc string) { 81 if desc == "" { 82 return 83 } 84 st.AddExtension(extEnumDesc, desc) 85 } 86 87 type schemaValidations struct { 88 current *spec.Schema 89 } 90 91 func (sv schemaValidations) SetMaximum(val float64, exclusive bool) { 92 sv.current.Maximum = &val 93 sv.current.ExclusiveMaximum = exclusive 94 } 95 96 func (sv schemaValidations) SetMinimum(val float64, exclusive bool) { 97 sv.current.Minimum = &val 98 sv.current.ExclusiveMinimum = exclusive 99 } 100 func (sv schemaValidations) SetMultipleOf(val float64) { sv.current.MultipleOf = &val } 101 func (sv schemaValidations) SetMinItems(val int64) { sv.current.MinItems = &val } 102 func (sv schemaValidations) SetMaxItems(val int64) { sv.current.MaxItems = &val } 103 func (sv schemaValidations) SetMinLength(val int64) { sv.current.MinLength = &val } 104 func (sv schemaValidations) SetMaxLength(val int64) { sv.current.MaxLength = &val } 105 func (sv schemaValidations) SetPattern(val string) { sv.current.Pattern = val } 106 func (sv schemaValidations) SetUnique(val bool) { sv.current.UniqueItems = val } 107 func (sv schemaValidations) SetDefault(val interface{}) { sv.current.Default = val } 108 func (sv schemaValidations) SetExample(val interface{}) { sv.current.Example = val } 109 func (sv schemaValidations) SetEnum(val string) { 110 sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Format: sv.current.Format, Type: sv.current.Type[0]}) 111 } 112 113 type schemaBuilder struct { 114 ctx *scanCtx 115 decl *entityDecl 116 GoName string 117 Name string 118 annotated bool 119 discovered []*entityDecl 120 postDecls []*entityDecl 121 } 122 123 func (s *schemaBuilder) inferNames() (goName string, name string) { 124 if s.GoName != "" { 125 goName, name = s.GoName, s.Name 126 return 127 } 128 129 goName = s.decl.Ident.Name 130 name = goName 131 defer func() { 132 s.GoName = goName 133 s.Name = name 134 }() 135 if s.decl.Comments == nil { 136 return 137 } 138 139 DECLS: 140 for _, cmt := range s.decl.Comments.List { 141 for _, ln := range strings.Split(cmt.Text, "\n") { 142 matches := rxModelOverride.FindStringSubmatch(ln) 143 if len(matches) > 0 { 144 s.annotated = true 145 } 146 if len(matches) > 1 && len(matches[1]) > 0 { 147 name = matches[1] 148 break DECLS 149 } 150 } 151 } 152 return 153 } 154 155 func (s *schemaBuilder) Build(definitions map[string]spec.Schema) error { 156 s.inferNames() 157 158 schema := definitions[s.Name] 159 err := s.buildFromDecl(s.decl, &schema) 160 if err != nil { 161 return err 162 } 163 definitions[s.Name] = schema 164 return nil 165 } 166 167 func (s *schemaBuilder) buildFromDecl(_ *entityDecl, schema *spec.Schema) error { 168 // analyze doc comment for the model 169 sp := new(sectionedParser) 170 sp.setTitle = func(lines []string) { schema.Title = joinDropLast(lines) } 171 sp.setDescription = func(lines []string) { 172 schema.Description = joinDropLast(lines) 173 enumDesc := getEnumDesc(schema.VendorExtensible.Extensions) 174 if enumDesc != "" { 175 schema.Description += "\n" + enumDesc 176 } 177 } 178 if err := sp.Parse(s.decl.Comments); err != nil { 179 return err 180 } 181 182 // if the type is marked to ignore, just return 183 if sp.ignored { 184 return nil 185 } 186 187 switch tpe := s.decl.Type.Obj().Type().(type) { 188 case *types.Basic: 189 debugLog("basic: %v", tpe.Name()) 190 case *types.Struct: 191 if err := s.buildFromStruct(s.decl, tpe, schema, make(map[string]string)); err != nil { 192 return err 193 } 194 case *types.Interface: 195 if err := s.buildFromInterface(s.decl, tpe, schema, make(map[string]string)); err != nil { 196 return err 197 } 198 case *types.Array: 199 debugLog("array: %v -> %v", s.decl.Ident.Name, tpe.Elem().String()) 200 case *types.Slice: 201 debugLog("slice: %v -> %v", s.decl.Ident.Name, tpe.Elem().String()) 202 case *types.Map: 203 debugLog("map: %v -> [%v]%v", s.decl.Ident.Name, tpe.Key().String(), tpe.Elem().String()) 204 case *types.Named: 205 o := tpe.Obj() 206 if o != nil { 207 debugLog("got the named type object: %s.%s | isAlias: %t | exported: %t", o.Pkg().Path(), o.Name(), o.IsAlias(), o.Exported()) 208 if o.Pkg().Name() == "time" && o.Name() == "Time" { 209 schema.Typed("string", "date-time") 210 return nil 211 } 212 213 ps := schemaTypable{schema, 0} 214 for { 215 ti := s.decl.Pkg.TypesInfo.Types[s.decl.Spec.Type] 216 if ti.IsBuiltin() { 217 break 218 } 219 if ti.IsType() { 220 if err := s.buildFromType(ti.Type, ps); err != nil { 221 return err 222 } 223 break 224 } 225 } 226 } 227 default: 228 log.Printf("WARNING: Missing parser for a %T, skipping model: %s\n", tpe, s.Name) 229 return nil 230 } 231 232 if schema.Ref.String() == "" { 233 if s.Name != s.GoName { 234 addExtension(&schema.VendorExtensible, "x-go-name", s.GoName) 235 } 236 addExtension(&schema.VendorExtensible, "x-go-package", s.decl.Type.Obj().Pkg().Path()) 237 } 238 return nil 239 } 240 241 func (s *schemaBuilder) buildFromTextMarshal(tpe types.Type, tgt swaggerTypable) error { 242 if typePtr, ok := tpe.(*types.Pointer); ok { 243 return s.buildFromTextMarshal(typePtr.Elem(), tgt) 244 } 245 246 typeNamed, ok := tpe.(*types.Named) 247 if !ok { 248 tgt.Typed("string", "") 249 return nil 250 } 251 252 tio := typeNamed.Obj() 253 if tio.Pkg() == nil && tio.Name() == "error" { 254 return swaggerSchemaForType(tio.Name(), tgt) 255 } 256 257 debugLog("named refined type %s.%s", tio.Pkg().Path(), tio.Name()) 258 pkg, found := s.ctx.PkgForType(tpe) 259 260 if strings.ToLower(tio.Name()) == "uuid" { 261 tgt.Typed("string", "uuid") 262 return nil 263 } 264 265 if !found { 266 // this must be a builtin 267 debugLog("skipping because package is nil: %s", tpe.String()) 268 return nil 269 } 270 if pkg.Name == "time" && tio.Name() == "Time" { 271 tgt.Typed("string", "date-time") 272 return nil 273 } 274 if pkg.PkgPath == "encoding/json" && tio.Name() == "RawMessage" { 275 tgt.Typed("object", "") 276 return nil 277 } 278 cmt, hasComments := s.ctx.FindComments(pkg, tio.Name()) 279 if !hasComments { 280 cmt = new(ast.CommentGroup) 281 } 282 283 if sfnm, isf := strfmtName(cmt); isf { 284 tgt.Typed("string", sfnm) 285 return nil 286 } 287 288 tgt.Typed("string", "") 289 return nil 290 } 291 292 func (s *schemaBuilder) buildFromType(tpe types.Type, tgt swaggerTypable) error { 293 pkg, err := importer.Default().Import("encoding") 294 if err != nil { 295 return nil 296 } 297 ifc := pkg.Scope().Lookup("TextMarshaler").Type().Underlying().(*types.Interface) 298 299 // check if the type implements encoding.TextMarshaler interface 300 isTextMarshaler := types.Implements(tpe, ifc) 301 if isTextMarshaler { 302 return s.buildFromTextMarshal(tpe, tgt) 303 } 304 305 switch titpe := tpe.(type) { 306 case *types.Basic: 307 return swaggerSchemaForType(titpe.String(), tgt) 308 case *types.Pointer: 309 return s.buildFromType(titpe.Elem(), tgt) 310 case *types.Struct: 311 return s.buildFromStruct(s.decl, titpe, tgt.Schema(), make(map[string]string)) 312 case *types.Interface: 313 return s.buildFromInterface(s.decl, titpe, tgt.Schema(), make(map[string]string)) 314 case *types.Slice: 315 return s.buildFromType(titpe.Elem(), tgt.Items()) 316 case *types.Array: 317 return s.buildFromType(titpe.Elem(), tgt.Items()) 318 case *types.Map: 319 // debugLog("map: %v -> [%v]%v", fld.Name(), ftpe.Key().String(), ftpe.Elem().String()) 320 // check if key is a string type, if not print a message 321 // and skip the map property. Only maps with string keys can go into additional properties 322 sch := tgt.Schema() 323 if sch == nil { 324 return errors.New("items doesn't support maps") 325 } 326 eleProp := schemaTypable{sch, tgt.Level()} 327 key := titpe.Key() 328 isTextMarshaler := types.Implements(key, ifc) 329 if key.Underlying().String() == "string" || isTextMarshaler { 330 return s.buildFromType(titpe.Elem(), eleProp.AdditionalProperties()) 331 } 332 case *types.Named: 333 tio := titpe.Obj() 334 if tio.Pkg() == nil && tio.Name() == "error" { 335 return swaggerSchemaForType(tio.Name(), tgt) 336 } 337 debugLog("named refined type %s.%s", tio.Pkg().Path(), tio.Name()) 338 pkg, found := s.ctx.PkgForType(tpe) 339 if !found { 340 // this must be a builtin 341 debugLog("skipping because package is nil: %s", tpe.String()) 342 return nil 343 } 344 if pkg.Name == "time" && tio.Name() == "Time" { 345 tgt.Typed("string", "date-time") 346 return nil 347 } 348 if pkg.PkgPath == "encoding/json" && tio.Name() == "RawMessage" { 349 tgt.Typed("object", "") 350 return nil 351 } 352 cmt, hasComments := s.ctx.FindComments(pkg, tio.Name()) 353 if !hasComments { 354 cmt = new(ast.CommentGroup) 355 } 356 357 if typeName, ok := typeName(cmt); ok { 358 _ = swaggerSchemaForType(typeName, tgt) 359 return nil 360 } 361 362 if s.decl.Spec.Assign.IsValid() { 363 return s.buildFromType(titpe.Underlying(), tgt) 364 } 365 366 switch utitpe := tpe.Underlying().(type) { 367 case *types.Struct: 368 if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { 369 if decl.Type.Obj().Pkg().Path() == "time" && decl.Type.Obj().Name() == "Time" { 370 tgt.Typed("string", "date-time") 371 return nil 372 } 373 if sfnm, isf := strfmtName(cmt); isf { 374 tgt.Typed("string", sfnm) 375 return nil 376 } 377 if typeName, ok := typeName(cmt); ok { 378 _ = swaggerSchemaForType(typeName, tgt) 379 return nil 380 } 381 382 return s.makeRef(decl, tgt) 383 } 384 case *types.Interface: 385 if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { 386 return s.makeRef(decl, tgt) 387 } 388 case *types.Basic: 389 if sfnm, isf := strfmtName(cmt); isf { 390 tgt.Typed("string", sfnm) 391 return nil 392 } 393 394 if enumName, ok := enumName(cmt); ok { 395 enumValues, enumDesces, _ := s.ctx.FindEnumValues(pkg, enumName) 396 if len(enumValues) > 0 { 397 tgt.WithEnum(enumValues...) 398 enumTypeName := reflect.TypeOf(enumValues[0]).String() 399 _ = swaggerSchemaForType(enumTypeName, tgt) 400 } 401 if len(enumDesces) > 0 { 402 tgt.WithEnumDescription(strings.Join(enumDesces, "\n")) 403 } 404 return nil 405 } 406 407 if defaultName, ok := defaultName(cmt); ok { 408 debugLog(defaultName) 409 return nil 410 } 411 412 if typeName, ok := typeName(cmt); ok { 413 _ = swaggerSchemaForType(typeName, tgt) 414 return nil 415 416 } 417 418 if isAliasParam(tgt) || aliasParam(cmt) { 419 err := swaggerSchemaForType(utitpe.Name(), tgt) 420 if err == nil { 421 return nil 422 } 423 } 424 if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { 425 return s.makeRef(decl, tgt) 426 } 427 return swaggerSchemaForType(utitpe.String(), tgt) 428 case *types.Array: 429 if sfnm, isf := strfmtName(cmt); isf { 430 if sfnm == "byte" { 431 tgt.Typed("string", sfnm) 432 return nil 433 } 434 if sfnm == "bsonobjectid" { 435 tgt.Typed("string", sfnm) 436 return nil 437 } 438 439 tgt.Items().Typed("string", sfnm) 440 return nil 441 } 442 if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { 443 return s.makeRef(decl, tgt) 444 } 445 return s.buildFromType(utitpe.Elem(), tgt.Items()) 446 case *types.Slice: 447 if sfnm, isf := strfmtName(cmt); isf { 448 if sfnm == "byte" { 449 tgt.Typed("string", sfnm) 450 return nil 451 } 452 tgt.Items().Typed("string", sfnm) 453 return nil 454 } 455 if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { 456 return s.makeRef(decl, tgt) 457 } 458 return s.buildFromType(utitpe.Elem(), tgt.Items()) 459 case *types.Map: 460 if decl, ok := s.ctx.FindModel(tio.Pkg().Path(), tio.Name()); ok { 461 return s.makeRef(decl, tgt) 462 } 463 return nil 464 465 default: 466 log.Printf("WARNING: can't figure out object type for named type (%T): %v [alias: %t]", tpe.Underlying(), tpe.Underlying(), titpe.Obj().IsAlias()) 467 468 return nil 469 } 470 default: 471 panic(fmt.Sprintf("WARNING: can't determine refined type %s (%T)", titpe.String(), titpe)) 472 } 473 474 return nil 475 } 476 477 func (s *schemaBuilder) buildFromInterface(decl *entityDecl, it *types.Interface, schema *spec.Schema, seen map[string]string) error { 478 if it.Empty() { 479 return nil 480 } 481 482 var ( 483 tgt *spec.Schema 484 hasAllOf bool 485 ) 486 487 var flist []*ast.Field 488 if specType, ok := decl.Spec.Type.(*ast.InterfaceType); ok { 489 flist = make([]*ast.Field, it.NumEmbeddeds()+it.NumExplicitMethods()) 490 copy(flist, specType.Methods.List) 491 // for i := range specType.Methods.List { 492 // flist[i] = specType.Methods.List[i] 493 // } 494 } 495 496 // First collect the embedded interfaces 497 // create refs when the embedded interface is decorated with an allOf annotation 498 for i := 0; i < it.NumEmbeddeds(); i++ { 499 fld := it.EmbeddedType(i) 500 501 switch ftpe := fld.(type) { 502 case *types.Named: 503 o := ftpe.Obj() 504 var afld *ast.Field 505 for _, an := range flist { 506 if len(an.Names) != 0 { 507 continue 508 } 509 510 tpp := decl.Pkg.TypesInfo.Types[an.Type] 511 if tpp.Type.String() != o.Type().String() { 512 continue 513 } 514 515 // decl. 516 debugLog("maybe interface field %s: %s(%T)", o.Name(), o.Type().String(), o.Type()) 517 afld = an 518 break 519 } 520 521 if afld == nil { 522 debugLog("can't find source associated with %s for %s", fld.String(), it.String()) 523 continue 524 } 525 526 // if the field is annotated with swagger:ignore, ignore it 527 if ignored(afld.Doc) { 528 continue 529 } 530 531 if !allOfMember(afld.Doc) { 532 var newSch spec.Schema 533 if err := s.buildEmbedded(o.Type(), &newSch, seen); err != nil { 534 return err 535 } 536 schema.AllOf = append(schema.AllOf, newSch) 537 hasAllOf = true 538 continue 539 } 540 541 hasAllOf = true 542 if tgt == nil { 543 tgt = &spec.Schema{} 544 } 545 var newSch spec.Schema 546 // when the embedded struct is annotated with swagger:allOf it will be used as allOf property 547 // otherwise the fields will just be included as normal properties 548 if err := s.buildAllOf(o.Type(), &newSch); err != nil { 549 return err 550 } 551 if afld.Doc != nil { 552 for _, cmt := range afld.Doc.List { 553 for _, ln := range strings.Split(cmt.Text, "\n") { 554 matches := rxAllOf.FindStringSubmatch(ln) 555 ml := len(matches) 556 if ml > 1 { 557 mv := matches[ml-1] 558 if mv != "" { 559 schema.AddExtension("x-class", mv) 560 } 561 } 562 } 563 } 564 } 565 566 schema.AllOf = append(schema.AllOf, newSch) 567 default: 568 log.Printf("WARNING: can't figure out object type for allOf named type (%T): %v", ftpe, ftpe.Underlying()) 569 } 570 debugLog("got embedded interface: %s {%T}", fld.String(), fld) 571 } 572 573 if tgt == nil { 574 tgt = schema 575 } 576 // We can finally build the actual schema for the struct 577 if tgt.Properties == nil { 578 tgt.Properties = make(map[string]spec.Schema) 579 } 580 tgt.Typed("object", "") 581 582 for i := 0; i < it.NumExplicitMethods(); i++ { 583 fld := it.ExplicitMethod(i) 584 if !fld.Exported() { 585 continue 586 } 587 sig, isSignature := fld.Type().(*types.Signature) 588 if !isSignature { 589 continue 590 } 591 if sig.Params().Len() > 0 { 592 continue 593 } 594 if sig.Results() == nil || sig.Results().Len() != 1 { 595 continue 596 } 597 598 var afld *ast.Field 599 ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos()) 600 // debugLog("got %d nodes (exact: %t)", len(ans), isExact) 601 for _, an := range ans { 602 at, valid := an.(*ast.Field) 603 if !valid { 604 continue 605 } 606 607 debugLog("maybe interface field %s: %s(%T)", fld.Name(), fld.Type().String(), fld.Type()) 608 afld = at 609 break 610 } 611 612 if afld == nil { 613 debugLog("can't find source associated with %s for %s", fld.String(), it.String()) 614 continue 615 } 616 617 // if the field is annotated with swagger:ignore, ignore it 618 if ignored(afld.Doc) { 619 continue 620 } 621 622 name := fld.Name() 623 if afld.Doc != nil { 624 for _, cmt := range afld.Doc.List { 625 for _, ln := range strings.Split(cmt.Text, "\n") { 626 matches := rxName.FindStringSubmatch(ln) 627 ml := len(matches) 628 if ml > 1 { 629 name = matches[ml-1] 630 } 631 } 632 } 633 } 634 ps := tgt.Properties[name] 635 if err := s.buildFromType(sig.Results().At(0).Type(), schemaTypable{&ps, 0}); err != nil { 636 return err 637 } 638 if sfName, isStrfmt := strfmtName(afld.Doc); isStrfmt { 639 ps.Typed("string", sfName) 640 ps.Ref = spec.Ref{} 641 ps.Items = nil 642 } 643 644 if err := s.createParser(name, tgt, &ps, afld).Parse(afld.Doc); err != nil { 645 return err 646 } 647 648 if ps.Ref.String() == "" && name != fld.Name() { 649 ps.AddExtension("x-go-name", fld.Name()) 650 } 651 652 seen[name] = fld.Name() 653 tgt.Properties[name] = ps 654 } 655 656 if tgt == nil { 657 return nil 658 } 659 if hasAllOf && len(tgt.Properties) > 0 { 660 schema.AllOf = append(schema.AllOf, *tgt) 661 } 662 for k := range tgt.Properties { 663 if _, ok := seen[k]; !ok { 664 delete(tgt.Properties, k) 665 } 666 } 667 return nil 668 } 669 670 func (s *schemaBuilder) buildFromStruct(decl *entityDecl, st *types.Struct, schema *spec.Schema, seen map[string]string) error { 671 s.ctx.FindComments(decl.Pkg, decl.Type.Obj().Name()) 672 cmt, hasComments := s.ctx.FindComments(decl.Pkg, decl.Type.Obj().Name()) 673 if !hasComments { 674 cmt = new(ast.CommentGroup) 675 } 676 if typeName, ok := typeName(cmt); ok { 677 _ = swaggerSchemaForType(typeName, schemaTypable{schema: schema}) 678 return nil 679 } 680 // First check for all of schemas 681 var tgt *spec.Schema 682 hasAllOf := false 683 684 for i := 0; i < st.NumFields(); i++ { 685 fld := st.Field(i) 686 if !fld.Anonymous() { 687 debugLog("skipping field %q for allOf scan because not anonymous", fld.Name()) 688 continue 689 } 690 tg := st.Tag(i) 691 692 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()) 693 var afld *ast.Field 694 ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos()) 695 // debugLog("got %d nodes (exact: %t)", len(ans), isExact) 696 for _, an := range ans { 697 at, valid := an.(*ast.Field) 698 if !valid { 699 continue 700 } 701 702 debugLog("maybe allof field %s: %s(%T) [%q]", fld.Name(), fld.Type().String(), fld.Type(), tg) 703 afld = at 704 break 705 } 706 707 if afld == nil { 708 debugLog("can't find source associated with %s for %s", fld.String(), st.String()) 709 continue 710 } 711 712 // if the field is annotated with swagger:ignore, ignore it 713 if ignored(afld.Doc) { 714 continue 715 } 716 717 _, ignore, _, err := parseJSONTag(afld) 718 if err != nil { 719 return err 720 } 721 if ignore { 722 continue 723 } 724 725 if !allOfMember(afld.Doc) { 726 if tgt == nil { 727 tgt = schema 728 } 729 if err := s.buildEmbedded(fld.Type(), tgt, seen); err != nil { 730 return err 731 } 732 continue 733 } 734 // if this created an allOf property then we have to rejig the schema var 735 // because all the fields collected that aren't from embedded structs should go in 736 // their own proper schema 737 // first process embedded structs in order of embedding 738 hasAllOf = true 739 if tgt == nil { 740 tgt = &spec.Schema{} 741 } 742 var newSch spec.Schema 743 // when the embedded struct is annotated with swagger:allOf it will be used as allOf property 744 // otherwise the fields will just be included as normal properties 745 if err := s.buildAllOf(fld.Type(), &newSch); err != nil { 746 return err 747 } 748 749 if afld.Doc != nil { 750 for _, cmt := range afld.Doc.List { 751 for _, ln := range strings.Split(cmt.Text, "\n") { 752 matches := rxAllOf.FindStringSubmatch(ln) 753 ml := len(matches) 754 if ml > 1 { 755 mv := matches[ml-1] 756 if mv != "" { 757 schema.AddExtension("x-class", mv) 758 } 759 } 760 } 761 } 762 } 763 764 schema.AllOf = append(schema.AllOf, newSch) 765 } 766 767 if tgt == nil { 768 if schema != nil { 769 tgt = schema 770 } else { 771 tgt = &spec.Schema{} 772 } 773 } 774 // We can finally build the actual schema for the struct 775 if tgt.Properties == nil { 776 tgt.Properties = make(map[string]spec.Schema) 777 } 778 tgt.Typed("object", "") 779 780 for i := 0; i < st.NumFields(); i++ { 781 fld := st.Field(i) 782 tg := st.Tag(i) 783 784 if fld.Embedded() { 785 continue 786 } 787 788 if !fld.Exported() { 789 debugLog("skipping field %s because it's not exported", fld.Name()) 790 continue 791 } 792 793 var afld *ast.Field 794 ans, _ := astutil.PathEnclosingInterval(decl.File, fld.Pos(), fld.Pos()) 795 // debugLog("got %d nodes (exact: %t)", len(ans), isExact) 796 for _, an := range ans { 797 at, valid := an.(*ast.Field) 798 if !valid { 799 continue 800 } 801 802 debugLog("field %s: %s(%T) [%q] ==> %s", fld.Name(), fld.Type().String(), fld.Type(), tg, at.Doc.Text()) 803 afld = at 804 break 805 } 806 807 if afld == nil { 808 debugLog("can't find source associated with %s", fld.String()) 809 continue 810 } 811 812 // if the field is annotated with swagger:ignore, ignore it 813 if ignored(afld.Doc) { 814 continue 815 } 816 817 name, ignore, isString, err := parseJSONTag(afld) 818 if err != nil { 819 return err 820 } 821 if ignore { 822 for seenTagName, seenFieldName := range seen { 823 if seenFieldName == fld.Name() { 824 delete(tgt.Properties, seenTagName) 825 break 826 } 827 } 828 continue 829 } 830 831 ps := tgt.Properties[name] 832 if err = s.buildFromType(fld.Type(), schemaTypable{&ps, 0}); err != nil { 833 return err 834 } 835 if isString { 836 ps.Typed("string", ps.Format) 837 ps.Ref = spec.Ref{} 838 ps.Items = nil 839 } 840 if sfName, isStrfmt := strfmtName(afld.Doc); isStrfmt { 841 ps.Typed("string", sfName) 842 ps.Ref = spec.Ref{} 843 ps.Items = nil 844 } 845 846 if err = s.createParser(name, tgt, &ps, afld).Parse(afld.Doc); err != nil { 847 return err 848 } 849 850 if ps.Ref.String() == "" && name != fld.Name() { 851 addExtension(&ps.VendorExtensible, "x-go-name", fld.Name()) 852 } 853 854 // we have 2 cases: 855 // 1. field with different name override tag 856 // 2. field with different name removes tag 857 // so we need to save both tag&name 858 seen[name] = fld.Name() 859 tgt.Properties[name] = ps 860 } 861 862 if tgt == nil { 863 return nil 864 } 865 if hasAllOf && len(tgt.Properties) > 0 { 866 schema.AllOf = append(schema.AllOf, *tgt) 867 } 868 for k := range tgt.Properties { 869 if _, ok := seen[k]; !ok { 870 delete(tgt.Properties, k) 871 } 872 } 873 return nil 874 } 875 876 func (s *schemaBuilder) buildAllOf(tpe types.Type, schema *spec.Schema) error { 877 debugLog("allOf %s", tpe.Underlying()) 878 switch ftpe := tpe.(type) { 879 case *types.Pointer: 880 return s.buildAllOf(ftpe.Elem(), schema) 881 case *types.Named: 882 switch utpe := ftpe.Underlying().(type) { 883 case *types.Struct: 884 decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) 885 if found { 886 if ftpe.Obj().Pkg().Path() == "time" && ftpe.Obj().Name() == "Time" { 887 schema.Typed("string", "date-time") 888 return nil 889 } 890 if sfnm, isf := strfmtName(decl.Comments); isf { 891 schema.Typed("string", sfnm) 892 return nil 893 } 894 if decl.HasModelAnnotation() { 895 return s.makeRef(decl, schemaTypable{schema, 0}) 896 } 897 return s.buildFromStruct(decl, utpe, schema, make(map[string]string)) 898 } 899 return fmt.Errorf("can't find source file for struct: %s", ftpe.String()) 900 case *types.Interface: 901 decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) 902 if found { 903 if sfnm, isf := strfmtName(decl.Comments); isf { 904 schema.Typed("string", sfnm) 905 return nil 906 } 907 if decl.HasModelAnnotation() { 908 return s.makeRef(decl, schemaTypable{schema, 0}) 909 } 910 return s.buildFromInterface(decl, utpe, schema, make(map[string]string)) 911 } 912 return fmt.Errorf("can't find source file for interface: %s", ftpe.String()) 913 default: 914 log.Printf("WARNING: can't figure out object type for allOf named type (%T): %v", ftpe, ftpe.Underlying()) 915 return fmt.Errorf("unable to locate source file for allOf %s", utpe.String()) 916 } 917 default: 918 log.Printf("WARNING: Missing allOf parser for a %T, skipping field", ftpe) 919 return fmt.Errorf("unable to resolve allOf member for: %v", ftpe) 920 } 921 } 922 923 func (s *schemaBuilder) buildEmbedded(tpe types.Type, schema *spec.Schema, seen map[string]string) error { 924 debugLog("embedded %s", tpe.Underlying()) 925 switch ftpe := tpe.(type) { 926 case *types.Pointer: 927 return s.buildEmbedded(ftpe.Elem(), schema, seen) 928 case *types.Named: 929 debugLog("embedded named type: %T", ftpe.Underlying()) 930 switch utpe := ftpe.Underlying().(type) { 931 case *types.Struct: 932 decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) 933 if found { 934 return s.buildFromStruct(decl, utpe, schema, seen) 935 } 936 return fmt.Errorf("can't find source file for struct: %s", ftpe.String()) 937 case *types.Interface: 938 decl, found := s.ctx.FindModel(ftpe.Obj().Pkg().Path(), ftpe.Obj().Name()) 939 if found { 940 return s.buildFromInterface(decl, utpe, schema, seen) 941 } 942 return fmt.Errorf("can't find source file for struct: %s", ftpe.String()) 943 default: 944 log.Printf("WARNING: can't figure out object type for embedded named type (%T): %v", ftpe, ftpe.Underlying()) 945 } 946 default: 947 log.Printf("WARNING: Missing embedded parser for a %T, skipping model\n", ftpe) 948 return nil 949 } 950 return nil 951 } 952 953 func (s *schemaBuilder) makeRef(decl *entityDecl, prop swaggerTypable) error { 954 nm, _ := decl.Names() 955 ref, err := spec.NewRef("#/definitions/" + nm) 956 if err != nil { 957 return err 958 } 959 prop.SetRef(ref) 960 s.postDecls = append(s.postDecls, decl) 961 return nil 962 } 963 964 func (s *schemaBuilder) createParser(nm string, schema, ps *spec.Schema, fld *ast.Field) *sectionedParser { 965 sp := new(sectionedParser) 966 967 schemeType, err := ps.Type.MarshalJSON() 968 if err != nil { 969 return nil 970 } 971 972 if ps.Ref.String() == "" { 973 sp.setDescription = func(lines []string) { 974 ps.Description = joinDropLast(lines) 975 enumDesc := getEnumDesc(ps.VendorExtensible.Extensions) 976 if enumDesc != "" { 977 ps.Description += "\n" + enumDesc 978 } 979 } 980 sp.taggers = []tagParser{ 981 newSingleLineTagParser("maximum", &setMaximum{schemaValidations{ps}, rxf(rxMaximumFmt, "")}), 982 newSingleLineTagParser("minimum", &setMinimum{schemaValidations{ps}, rxf(rxMinimumFmt, "")}), 983 newSingleLineTagParser("multipleOf", &setMultipleOf{schemaValidations{ps}, rxf(rxMultipleOfFmt, "")}), 984 newSingleLineTagParser("minLength", &setMinLength{schemaValidations{ps}, rxf(rxMinLengthFmt, "")}), 985 newSingleLineTagParser("maxLength", &setMaxLength{schemaValidations{ps}, rxf(rxMaxLengthFmt, "")}), 986 newSingleLineTagParser("pattern", &setPattern{schemaValidations{ps}, rxf(rxPatternFmt, "")}), 987 newSingleLineTagParser("minItems", &setMinItems{schemaValidations{ps}, rxf(rxMinItemsFmt, "")}), 988 newSingleLineTagParser("maxItems", &setMaxItems{schemaValidations{ps}, rxf(rxMaxItemsFmt, "")}), 989 newSingleLineTagParser("unique", &setUnique{schemaValidations{ps}, rxf(rxUniqueFmt, "")}), 990 newSingleLineTagParser("enum", &setEnum{schemaValidations{ps}, rxf(rxEnumFmt, "")}), 991 newSingleLineTagParser("default", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}), 992 newSingleLineTagParser("type", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}), 993 newSingleLineTagParser("example", &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxExampleFmt, "")}), 994 newSingleLineTagParser("required", &setRequiredSchema{schema, nm}), 995 newSingleLineTagParser("readOnly", &setReadOnlySchema{ps}), 996 newSingleLineTagParser("discriminator", &setDiscriminator{schema, nm}), 997 newMultiLineTagParser("YAMLExtensionsBlock", newYamlParser(rxExtensions, schemaVendorExtensibleSetter(ps)), true), 998 } 999 1000 itemsTaggers := func(items *spec.Schema, level int) []tagParser { 1001 schemeType, err := items.Type.MarshalJSON() 1002 if err != nil { 1003 return nil 1004 } 1005 // the expression is 1-index based not 0-index 1006 itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1) 1007 return []tagParser{ 1008 newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{schemaValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}), 1009 newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{schemaValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}), 1010 newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{schemaValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}), 1011 newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{schemaValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}), 1012 newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{schemaValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}), 1013 newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{schemaValidations{items}, rxf(rxPatternFmt, itemsPrefix)}), 1014 newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{schemaValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}), 1015 newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{schemaValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}), 1016 newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{schemaValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}), 1017 newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{schemaValidations{items}, rxf(rxEnumFmt, itemsPrefix)}), 1018 newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}), 1019 newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxExampleFmt, itemsPrefix)}), 1020 } 1021 } 1022 1023 var parseArrayTypes func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error) 1024 parseArrayTypes = func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error) { 1025 if items == nil || items.Schema == nil { 1026 return []tagParser{}, nil 1027 } 1028 switch iftpe := expr.(type) { 1029 case *ast.ArrayType: 1030 eleTaggers := itemsTaggers(items.Schema, level) 1031 sp.taggers = append(eleTaggers, sp.taggers...) 1032 otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Schema.Items, level+1) 1033 if err != nil { 1034 return nil, err 1035 } 1036 return otherTaggers, nil 1037 case *ast.Ident: 1038 taggers := []tagParser{} 1039 if iftpe.Obj == nil { 1040 taggers = itemsTaggers(items.Schema, level) 1041 } 1042 otherTaggers, err := parseArrayTypes(expr, items.Schema.Items, level+1) 1043 if err != nil { 1044 return nil, err 1045 } 1046 return append(taggers, otherTaggers...), nil 1047 case *ast.StarExpr: 1048 otherTaggers, err := parseArrayTypes(iftpe.X, items, level) 1049 if err != nil { 1050 return nil, err 1051 } 1052 return otherTaggers, nil 1053 default: 1054 return nil, fmt.Errorf("unknown field type ele for %q", nm) 1055 } 1056 } 1057 // check if this is a primitive, if so parse the validations from the 1058 // doc comments of the slice declaration. 1059 if ftped, ok := fld.Type.(*ast.ArrayType); ok { 1060 taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0) 1061 if err != nil { 1062 return sp 1063 } 1064 sp.taggers = append(taggers, sp.taggers...) 1065 } 1066 1067 } else { 1068 sp.taggers = []tagParser{ 1069 newSingleLineTagParser("required", &setRequiredSchema{schema, nm}), 1070 } 1071 } 1072 return sp 1073 } 1074 1075 func schemaVendorExtensibleSetter(meta *spec.Schema) func(json.RawMessage) error { 1076 return func(jsonValue json.RawMessage) error { 1077 var jsonData spec.Extensions 1078 err := json.Unmarshal(jsonValue, &jsonData) 1079 if err != nil { 1080 return err 1081 } 1082 for k := range jsonData { 1083 if !rxAllowedExtensions.MatchString(k) { 1084 return fmt.Errorf("invalid schema extension name, should start from `x-`: %s", k) 1085 } 1086 } 1087 meta.Extensions = jsonData 1088 return nil 1089 } 1090 } 1091 1092 type tagOptions []string 1093 1094 func (t tagOptions) Contain(option string) bool { 1095 for i := 1; i < len(t); i++ { 1096 if t[i] == option { 1097 return true 1098 } 1099 } 1100 return false 1101 } 1102 1103 func (t tagOptions) Name() string { 1104 return t[0] 1105 } 1106 1107 func parseJSONTag(field *ast.Field) (name string, ignore bool, isString bool, err error) { 1108 if len(field.Names) > 0 { 1109 name = field.Names[0].Name 1110 } 1111 if field.Tag == nil || len(strings.TrimSpace(field.Tag.Value)) == 0 { 1112 return name, false, false, nil 1113 } 1114 1115 tv, err := strconv.Unquote(field.Tag.Value) 1116 if err != nil { 1117 return name, false, false, err 1118 } 1119 1120 if strings.TrimSpace(tv) != "" { 1121 st := reflect.StructTag(tv) 1122 jsonParts := tagOptions(strings.Split(st.Get("json"), ",")) 1123 1124 if jsonParts.Contain("string") { 1125 // Need to check if the field type is a scalar. Otherwise, the 1126 // ",string" directive doesn't apply. 1127 isString = isFieldStringable(field.Type) 1128 } 1129 1130 switch jsonParts.Name() { 1131 case "-": 1132 return name, true, isString, nil 1133 case "": 1134 return name, false, isString, nil 1135 default: 1136 return jsonParts.Name(), false, isString, nil 1137 } 1138 } 1139 return name, false, false, nil 1140 } 1141 1142 // isFieldStringable check if the field type is a scalar. If the field type is 1143 // *ast.StarExpr and is pointer type, check if it refers to a scalar. 1144 // Otherwise, the ",string" directive doesn't apply. 1145 func isFieldStringable(tpe ast.Expr) bool { 1146 if ident, ok := tpe.(*ast.Ident); ok { 1147 switch ident.Name { 1148 case "int", "int8", "int16", "int32", "int64", 1149 "uint", "uint8", "uint16", "uint32", "uint64", 1150 "float64", "string", "bool": 1151 return true 1152 } 1153 } else if starExpr, ok := tpe.(*ast.StarExpr); ok { 1154 return isFieldStringable(starExpr.X) 1155 } else { 1156 return false 1157 } 1158 return false 1159 }