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