github.com/jamescostian/go-swagger@v0.30.4-0.20221130163922-68364d6b567b/scan/schema.go (about) 1 //go:build !go1.11 2 // +build !go1.11 3 4 // Copyright 2015 go-swagger maintainers 5 // 6 // Licensed under the Apache License, Version 2.0 (the "License"); 7 // you may not use this file except in compliance with the License. 8 // You may obtain a copy of the License at 9 // 10 // http://www.apache.org/licenses/LICENSE-2.0 11 // 12 // Unless required by applicable law or agreed to in writing, software 13 // distributed under the License is distributed on an "AS IS" BASIS, 14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 // See the License for the specific language governing permissions and 16 // limitations under the License. 17 18 package scan 19 20 import ( 21 "fmt" 22 "go/ast" 23 "log" 24 "os" 25 "path/filepath" 26 "reflect" 27 "runtime" 28 "strconv" 29 "strings" 30 31 "golang.org/x/tools/go/loader" 32 33 "github.com/go-openapi/spec" 34 ) 35 36 func addExtension(ve *spec.VendorExtensible, key string, value interface{}) { 37 if os.Getenv("SWAGGER_GENERATE_EXTENSION") == "false" { 38 return 39 } 40 41 ve.AddExtension(key, value) 42 } 43 44 type schemaTypable struct { 45 schema *spec.Schema 46 level int 47 } 48 49 func (st schemaTypable) Typed(tpe, format string) { 50 st.schema.Typed(tpe, format) 51 } 52 53 func (st schemaTypable) SetRef(ref spec.Ref) { 54 st.schema.Ref = ref 55 } 56 57 func (st schemaTypable) Schema() *spec.Schema { 58 return st.schema 59 } 60 61 func (st schemaTypable) Items() swaggerTypable { 62 if st.schema.Items == nil { 63 st.schema.Items = new(spec.SchemaOrArray) 64 } 65 if st.schema.Items.Schema == nil { 66 st.schema.Items.Schema = new(spec.Schema) 67 } 68 69 st.schema.Typed("array", "") 70 return schemaTypable{st.schema.Items.Schema, st.level + 1} 71 } 72 73 func (st schemaTypable) AdditionalProperties() swaggerTypable { 74 if st.schema.AdditionalProperties == nil { 75 st.schema.AdditionalProperties = new(spec.SchemaOrBool) 76 } 77 if st.schema.AdditionalProperties.Schema == nil { 78 st.schema.AdditionalProperties.Schema = new(spec.Schema) 79 } 80 81 st.schema.Typed("object", "") 82 return schemaTypable{st.schema.AdditionalProperties.Schema, st.level + 1} 83 } 84 85 func (st schemaTypable) Level() int { return st.level } 86 87 func (st schemaTypable) WithEnum(values ...interface{}) { 88 st.schema.WithEnum(values...) 89 } 90 91 type schemaValidations struct { 92 current *spec.Schema 93 } 94 95 func (sv schemaValidations) SetMaximum(val float64, exclusive bool) { 96 sv.current.Maximum = &val 97 sv.current.ExclusiveMaximum = exclusive 98 } 99 func (sv schemaValidations) SetMinimum(val float64, exclusive bool) { 100 sv.current.Minimum = &val 101 sv.current.ExclusiveMinimum = exclusive 102 } 103 func (sv schemaValidations) SetMultipleOf(val float64) { sv.current.MultipleOf = &val } 104 func (sv schemaValidations) SetMinItems(val int64) { sv.current.MinItems = &val } 105 func (sv schemaValidations) SetMaxItems(val int64) { sv.current.MaxItems = &val } 106 func (sv schemaValidations) SetMinLength(val int64) { sv.current.MinLength = &val } 107 func (sv schemaValidations) SetMaxLength(val int64) { sv.current.MaxLength = &val } 108 func (sv schemaValidations) SetPattern(val string) { sv.current.Pattern = val } 109 func (sv schemaValidations) SetUnique(val bool) { sv.current.UniqueItems = val } 110 func (sv schemaValidations) SetDefault(val interface{}) { sv.current.Default = val } 111 func (sv schemaValidations) SetExample(val interface{}) { sv.current.Example = val } 112 func (sv schemaValidations) SetEnum(val string) { 113 sv.current.Enum = parseEnum(val, &spec.SimpleSchema{Format: sv.current.Format, Type: sv.current.Type[0]}) 114 } 115 116 type schemaDecl struct { 117 File *ast.File 118 Decl *ast.GenDecl 119 TypeSpec *ast.TypeSpec 120 GoName string 121 Name string 122 annotated bool 123 } 124 125 func newSchemaDecl(file *ast.File, decl *ast.GenDecl, ts *ast.TypeSpec) *schemaDecl { 126 sd := &schemaDecl{ 127 File: file, 128 Decl: decl, 129 TypeSpec: ts, 130 } 131 sd.inferNames() 132 return sd 133 } 134 135 func (sd *schemaDecl) hasAnnotation() bool { 136 sd.inferNames() 137 return sd.annotated 138 } 139 140 func (sd *schemaDecl) inferNames() (goName string, name string) { 141 if sd.GoName != "" { 142 goName, name = sd.GoName, sd.Name 143 return 144 } 145 goName = sd.TypeSpec.Name.Name 146 name = goName 147 if sd.Decl.Doc != nil { 148 DECLS: 149 for _, cmt := range sd.Decl.Doc.List { 150 for _, ln := range strings.Split(cmt.Text, "\n") { 151 matches := rxModelOverride.FindStringSubmatch(ln) 152 if len(matches) > 0 { 153 sd.annotated = true 154 } 155 if len(matches) > 1 && len(matches[1]) > 0 { 156 name = matches[1] 157 break DECLS 158 } 159 } 160 } 161 } 162 sd.GoName = goName 163 sd.Name = name 164 return 165 } 166 167 type schemaParser struct { 168 program *loader.Program 169 postDecls []schemaDecl 170 known map[string]spec.Schema 171 discovered *schemaDecl 172 } 173 174 func newSchemaParser(prog *loader.Program) *schemaParser { 175 scp := new(schemaParser) 176 scp.program = prog 177 scp.known = make(map[string]spec.Schema) 178 return scp 179 } 180 181 func (scp *schemaParser) Parse(gofile *ast.File, target interface{}) error { 182 tgt := target.(map[string]spec.Schema) 183 for _, decl := range gofile.Decls { 184 gd, ok := decl.(*ast.GenDecl) 185 if !ok { 186 continue 187 } 188 for _, spc := range gd.Specs { 189 if ts, ok := spc.(*ast.TypeSpec); ok { 190 sd := newSchemaDecl(gofile, gd, ts) 191 if err := scp.parseDecl(tgt, sd); err != nil { 192 return err 193 } 194 } 195 } 196 } 197 return nil 198 } 199 200 func (scp *schemaParser) parseDecl(definitions map[string]spec.Schema, decl *schemaDecl) error { 201 // check if there is a swagger:model tag that is followed by a word, 202 // this word is the type name for swagger 203 // the package and type are recorded in the extensions 204 // once type name is found convert it to a schema, by looking up the schema in the 205 // definitions dictionary that got passed into this parse method 206 207 // if our schemaParser is parsing a discovered schemaDecl and it does not match 208 // the current schemaDecl we can skip parsing. 209 if scp.discovered != nil && scp.discovered.Name != decl.Name { 210 return nil 211 } 212 213 decl.inferNames() 214 schema := definitions[decl.Name] 215 schPtr := &schema 216 217 // analyze doc comment for the model 218 sp := new(sectionedParser) 219 sp.setTitle = func(lines []string) { schema.Title = joinDropLast(lines) } 220 sp.setDescription = func(lines []string) { schema.Description = joinDropLast(lines) } 221 if err := sp.Parse(decl.Decl.Doc); err != nil { 222 return err 223 } 224 225 // if the type is marked to ignore, just return 226 if sp.ignored { 227 return nil 228 } 229 230 // analyze struct body for fields etc 231 // each exported struct field: 232 // * gets a type mapped to a go primitive 233 // * perhaps gets a format 234 // * has to document the validations that apply for the type and the field 235 // * when the struct field points to a model it becomes a ref: #/definitions/ModelName 236 // * the first line of the comment is the title 237 // * the following lines are the description 238 switch tpe := decl.TypeSpec.Type.(type) { 239 case *ast.StructType: 240 if err := scp.parseStructType(decl.File, schPtr, tpe, make(map[string]string)); err != nil { 241 return err 242 } 243 case *ast.InterfaceType: 244 if err := scp.parseInterfaceType(decl.File, schPtr, tpe, make(map[string]string)); err != nil { 245 return err 246 } 247 case *ast.Ident: 248 prop := &schemaTypable{schPtr, 0} 249 if strfmtName, ok := strfmtName(decl.Decl.Doc); ok { 250 prop.Typed("string", strfmtName) 251 } else { 252 if err := scp.parseNamedType(decl.File, tpe, prop); err != nil { 253 return err 254 } 255 } 256 if enumName, ok := enumName(decl.Decl.Doc); ok { 257 var enumValues = getEnumValues(decl.File, enumName) 258 if len(enumValues) > 0 { 259 var typeName = reflect.TypeOf(enumValues[0]).String() 260 prop.WithEnum(enumValues...) 261 262 err := swaggerSchemaForType(typeName, prop) 263 if err != nil { 264 return fmt.Errorf("file %s, error is: %v", decl.File.Name, err) 265 } 266 } 267 } 268 case *ast.SelectorExpr: 269 prop := &schemaTypable{schPtr, 0} 270 if strfmtName, ok := strfmtName(decl.Decl.Doc); ok { 271 prop.Typed("string", strfmtName) 272 } else { 273 if err := scp.parseNamedType(decl.File, tpe, prop); err != nil { 274 return err 275 } 276 } 277 278 case *ast.ArrayType: 279 prop := &schemaTypable{schPtr, 0} 280 if strfmtName, ok := strfmtName(decl.Decl.Doc); ok { 281 prop.Items().Typed("string", strfmtName) 282 } else { 283 if err := scp.parseNamedType(decl.File, tpe, &schemaTypable{schPtr, 0}); err != nil { 284 return err 285 } 286 } 287 288 case *ast.MapType: 289 prop := &schemaTypable{schPtr, 0} 290 if strfmtName, ok := strfmtName(decl.Decl.Doc); ok { 291 prop.AdditionalProperties().Typed("string", strfmtName) 292 } else { 293 if err := scp.parseNamedType(decl.File, tpe, &schemaTypable{schPtr, 0}); err != nil { 294 return err 295 } 296 } 297 default: 298 log.Printf("WARNING: Missing parser for a %T, skipping model: %s\n", tpe, decl.Name) 299 return nil 300 } 301 302 if schPtr.Ref.String() == "" { 303 if decl.Name != decl.GoName { 304 addExtension(&schPtr.VendorExtensible, "x-go-name", decl.GoName) 305 } 306 for _, pkgInfo := range scp.program.AllPackages { 307 if pkgInfo.Importable { 308 for _, fil := range pkgInfo.Files { 309 if fil.Pos() == decl.File.Pos() { 310 addExtension(&schPtr.VendorExtensible, "x-go-package", pkgInfo.Pkg.Path()) 311 } 312 } 313 } 314 } 315 } 316 definitions[decl.Name] = schema 317 return nil 318 } 319 320 func (scp *schemaParser) parseNamedType(gofile *ast.File, expr ast.Expr, prop swaggerTypable) error { 321 switch ftpe := expr.(type) { 322 case *ast.Ident: // simple value 323 pkg, err := scp.packageForFile(gofile, ftpe) 324 if err != nil { 325 return err 326 } 327 return scp.parseIdentProperty(pkg, ftpe, prop) 328 329 case *ast.StarExpr: // pointer to something, optional by default 330 if err := scp.parseNamedType(gofile, ftpe.X, prop); err != nil { 331 return err 332 } 333 334 case *ast.ArrayType: // slice type 335 if err := scp.parseNamedType(gofile, ftpe.Elt, prop.Items()); err != nil { 336 return err 337 } 338 339 case *ast.StructType: 340 schema := prop.Schema() 341 if schema == nil { 342 return fmt.Errorf("items doesn't support embedded structs") 343 } 344 return scp.parseStructType(gofile, prop.Schema(), ftpe, make(map[string]string)) 345 346 case *ast.SelectorExpr: 347 err := scp.typeForSelector(gofile, ftpe, prop) 348 return err 349 350 case *ast.MapType: 351 // check if key is a string type, if not print a message 352 // and skip the map property. Only maps with string keys can go into additional properties 353 sch := prop.Schema() 354 if sch == nil { 355 return fmt.Errorf("items doesn't support maps") 356 } 357 if keyIdent, ok := ftpe.Key.(*ast.Ident); sch != nil && ok { 358 if keyIdent.Name == "string" { 359 if sch.AdditionalProperties == nil { 360 sch.AdditionalProperties = new(spec.SchemaOrBool) 361 } 362 sch.AdditionalProperties.Allows = false 363 if sch.AdditionalProperties.Schema == nil { 364 sch.AdditionalProperties.Schema = new(spec.Schema) 365 } 366 if err := scp.parseNamedType(gofile, ftpe.Value, schemaTypable{sch.AdditionalProperties.Schema, 0}); err != nil { 367 return err 368 } 369 sch.Typed("object", "") 370 } 371 } 372 373 case *ast.InterfaceType: 374 prop.Schema().Typed("object", "") 375 default: 376 pos := "unknown file:unknown position" 377 if scp != nil { 378 if scp.program != nil { 379 if scp.program.Fset != nil { 380 pos = scp.program.Fset.Position(expr.Pos()).String() 381 } 382 } 383 } 384 return fmt.Errorf("expr (%s) is unsupported for a schema", pos) 385 } 386 return nil 387 } 388 389 func (scp *schemaParser) parseEmbeddedType(gofile *ast.File, schema *spec.Schema, expr ast.Expr, seenPreviously map[string]string) error { 390 switch tpe := expr.(type) { 391 case *ast.Ident: 392 // do lookup of type 393 // take primitives into account, they should result in an error for swagger 394 pkg, err := scp.packageForFile(gofile, tpe) 395 if err != nil { 396 return err 397 } 398 file, _, ts, err := findSourceFile(pkg, tpe.Name) 399 if err != nil { 400 return err 401 } 402 403 switch st := ts.Type.(type) { 404 case *ast.StructType: 405 return scp.parseStructType(file, schema, st, seenPreviously) 406 case *ast.InterfaceType: 407 return scp.parseInterfaceType(file, schema, st, seenPreviously) 408 default: 409 prop := &schemaTypable{schema, 0} 410 return scp.parseNamedType(gofile, st, prop) 411 } 412 413 case *ast.SelectorExpr: 414 // look up package, file and then type 415 pkg, err := scp.packageForSelector(gofile, tpe.X) 416 if err != nil { 417 return fmt.Errorf("embedded struct: %v", err) 418 } 419 file, _, ts, err := findSourceFile(pkg, tpe.Sel.Name) 420 if err != nil { 421 return fmt.Errorf("embedded struct: %v", err) 422 } 423 if st, ok := ts.Type.(*ast.StructType); ok { 424 return scp.parseStructType(file, schema, st, seenPreviously) 425 } 426 if st, ok := ts.Type.(*ast.InterfaceType); ok { 427 return scp.parseInterfaceType(file, schema, st, seenPreviously) 428 } 429 case *ast.StarExpr: 430 return scp.parseEmbeddedType(gofile, schema, tpe.X, seenPreviously) 431 default: 432 return fmt.Errorf( 433 "parseEmbeddedType: unsupported type %v at position %#v", 434 expr, 435 scp.program.Fset.Position(tpe.Pos()), 436 ) 437 } 438 return fmt.Errorf("unable to resolve embedded struct for: %v", expr) 439 } 440 441 func (scp *schemaParser) parseAllOfMember(gofile *ast.File, schema *spec.Schema, expr ast.Expr, seenPreviously map[string]string) error { 442 // TODO: check if struct is annotated with swagger:model or known in the definitions otherwise 443 var pkg *loader.PackageInfo 444 var file *ast.File 445 var gd *ast.GenDecl 446 var ts *ast.TypeSpec 447 var err error 448 449 switch tpe := expr.(type) { 450 case *ast.Ident: 451 // do lookup of type 452 // take primitives into account, they should result in an error for swagger 453 pkg, err = scp.packageForFile(gofile, tpe) 454 if err != nil { 455 return err 456 } 457 file, gd, ts, err = findSourceFile(pkg, tpe.Name) 458 if err != nil { 459 return err 460 } 461 462 case *ast.SelectorExpr: 463 // look up package, file and then type 464 pkg, err = scp.packageForSelector(gofile, tpe.X) 465 if err != nil { 466 return fmt.Errorf("embedded struct: %v", err) 467 } 468 file, gd, ts, err = findSourceFile(pkg, tpe.Sel.Name) 469 if err != nil { 470 return fmt.Errorf("embedded struct: %v", err) 471 } 472 default: 473 return fmt.Errorf("unable to resolve allOf member for: %v", expr) 474 } 475 476 sd := newSchemaDecl(file, gd, ts) 477 if sd.hasAnnotation() && pkg.String() != "time" && ts.Name.Name != "Time" { 478 ref, err := spec.NewRef("#/definitions/" + sd.Name) 479 if err != nil { 480 return err 481 } 482 schema.Ref = ref 483 scp.postDecls = append(scp.postDecls, *sd) 484 } else { 485 switch st := ts.Type.(type) { 486 case *ast.StructType: 487 return scp.parseStructType(file, schema, st, seenPreviously) 488 case *ast.InterfaceType: 489 return scp.parseInterfaceType(file, schema, st, seenPreviously) 490 } 491 } 492 493 return nil 494 } 495 func (scp *schemaParser) parseInterfaceType(gofile *ast.File, bschema *spec.Schema, tpe *ast.InterfaceType, seenPreviously map[string]string) error { 496 if tpe.Methods == nil { 497 return nil 498 } 499 500 // first check if this has embedded interfaces, if so make sure to refer to those by ref 501 // when they are decorated with an allOf annotation 502 // go over the method list again and this time collect the nullary methods and parse the comments 503 // as if they are properties on a struct 504 var schema *spec.Schema 505 seenProperties := seenPreviously 506 hasAllOf := false 507 508 for _, fld := range tpe.Methods.List { 509 if len(fld.Names) == 0 { 510 // if this created an allOf property then we have to rejig the schema var 511 // because all the fields collected that aren't from embedded structs should go in 512 // their own proper schema 513 // first process embedded structs in order of embedding 514 if allOfMember(fld.Doc) { 515 hasAllOf = true 516 if schema == nil { 517 schema = new(spec.Schema) 518 } 519 var newSch spec.Schema 520 // when the embedded struct is annotated with swagger:allOf it will be used as allOf property 521 // otherwise the fields will just be included as normal properties 522 if err := scp.parseAllOfMember(gofile, &newSch, fld.Type, seenProperties); err != nil { 523 return err 524 } 525 526 if fld.Doc != nil { 527 for _, cmt := range fld.Doc.List { 528 for _, ln := range strings.Split(cmt.Text, "\n") { 529 matches := rxAllOf.FindStringSubmatch(ln) 530 ml := len(matches) 531 if ml > 1 { 532 mv := matches[ml-1] 533 if mv != "" { 534 addExtension(&bschema.VendorExtensible, "x-class", mv) 535 } 536 } 537 } 538 } 539 } 540 541 bschema.AllOf = append(bschema.AllOf, newSch) 542 continue 543 } 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 := scp.parseEmbeddedType(gofile, &newSch, fld.Type, seenProperties); err != nil { 549 return err 550 } 551 bschema.AllOf = append(bschema.AllOf, newSch) 552 hasAllOf = true 553 } 554 } 555 556 if schema == nil { 557 schema = bschema 558 } 559 // then add and possibly override values 560 if schema.Properties == nil { 561 schema.Properties = make(map[string]spec.Schema) 562 } 563 schema.Typed("object", "") 564 for _, fld := range tpe.Methods.List { 565 if mtpe, ok := fld.Type.(*ast.FuncType); ok && mtpe.Params.NumFields() == 0 && mtpe.Results.NumFields() == 1 { 566 gnm := fld.Names[0].Name 567 nm := gnm 568 if fld.Doc != nil { 569 for _, cmt := range fld.Doc.List { 570 for _, ln := range strings.Split(cmt.Text, "\n") { 571 matches := rxName.FindStringSubmatch(ln) 572 ml := len(matches) 573 if ml > 1 { 574 nm = matches[ml-1] 575 } 576 } 577 } 578 } 579 580 ps := schema.Properties[nm] 581 if err := parseProperty(scp, gofile, mtpe.Results.List[0].Type, schemaTypable{&ps, 0}); err != nil { 582 return err 583 } 584 585 if err := scp.createParser(nm, schema, &ps, fld).Parse(fld.Doc); err != nil { 586 return err 587 } 588 589 if ps.Ref.String() == "" && nm != gnm { 590 addExtension(&ps.VendorExtensible, "x-go-name", gnm) 591 } 592 seenProperties[nm] = gnm 593 schema.Properties[nm] = ps 594 } 595 596 } 597 if schema != nil && hasAllOf && len(schema.Properties) > 0 { 598 bschema.AllOf = append(bschema.AllOf, *schema) 599 } 600 for k := range schema.Properties { 601 if _, ok := seenProperties[k]; !ok { 602 delete(schema.Properties, k) 603 } 604 } 605 return nil 606 } 607 608 func (scp *schemaParser) parseStructType(gofile *ast.File, bschema *spec.Schema, tpe *ast.StructType, seenPreviously map[string]string) error { 609 if tpe.Fields == nil { 610 return nil 611 } 612 var schema *spec.Schema 613 seenProperties := seenPreviously 614 hasAllOf := false 615 616 for _, fld := range tpe.Fields.List { 617 if len(fld.Names) == 0 { 618 // if the field is annotated with swagger:ignore, ignore it 619 if ignored(fld.Doc) { 620 continue 621 } 622 623 _, ignore, _, err := parseJSONTag(fld) 624 if err != nil { 625 return err 626 } 627 if ignore { 628 continue 629 } 630 631 // if this created an allOf property then we have to rejig the schema var 632 // because all the fields collected that aren't from embedded structs should go in 633 // their own proper schema 634 // first process embedded structs in order of embedding 635 if allOfMember(fld.Doc) { 636 hasAllOf = true 637 if schema == nil { 638 schema = new(spec.Schema) 639 } 640 var newSch spec.Schema 641 // when the embedded struct is annotated with swagger:allOf it will be used as allOf property 642 // otherwise the fields will just be included as normal properties 643 if err := scp.parseAllOfMember(gofile, &newSch, fld.Type, seenProperties); err != nil { 644 return err 645 } 646 647 if fld.Doc != nil { 648 for _, cmt := range fld.Doc.List { 649 for _, ln := range strings.Split(cmt.Text, "\n") { 650 matches := rxAllOf.FindStringSubmatch(ln) 651 ml := len(matches) 652 if ml > 1 { 653 mv := matches[ml-1] 654 if mv != "" { 655 addExtension(&bschema.VendorExtensible, "x-class", mv) 656 } 657 } 658 } 659 } 660 } 661 662 bschema.AllOf = append(bschema.AllOf, newSch) 663 continue 664 } 665 if schema == nil { 666 schema = bschema 667 } 668 669 // when the embedded struct is annotated with swagger:allOf it will be used as allOf property 670 // otherwise the fields will just be included as normal properties 671 if err := scp.parseEmbeddedType(gofile, schema, fld.Type, seenProperties); err != nil { 672 return err 673 } 674 } 675 } 676 if schema == nil { 677 schema = bschema 678 } 679 680 // then add and possibly override values 681 if schema.Properties == nil { 682 schema.Properties = make(map[string]spec.Schema) 683 } 684 schema.Typed("object", "") 685 for _, fld := range tpe.Fields.List { 686 if len(fld.Names) > 0 && fld.Names[0] != nil && fld.Names[0].IsExported() { 687 // if the field is annotated with swagger:ignore, ignore it 688 if ignored(fld.Doc) { 689 continue 690 } 691 692 gnm := fld.Names[0].Name 693 nm, ignore, isString, err := parseJSONTag(fld) 694 if err != nil { 695 return err 696 } 697 if ignore { 698 for seenTagName, seenFieldName := range seenPreviously { 699 if seenFieldName == gnm { 700 delete(schema.Properties, seenTagName) 701 break 702 } 703 } 704 continue 705 } 706 707 ps := schema.Properties[nm] 708 if err := parseProperty(scp, gofile, fld.Type, schemaTypable{&ps, 0}); err != nil { 709 return err 710 } 711 if isString { 712 ps.Typed("string", ps.Format) 713 ps.Ref = spec.Ref{} 714 } 715 if strfmtName, ok := strfmtName(fld.Doc); ok { 716 ps.Typed("string", strfmtName) 717 ps.Ref = spec.Ref{} 718 } 719 720 if err := scp.createParser(nm, schema, &ps, fld).Parse(fld.Doc); err != nil { 721 return err 722 } 723 724 if ps.Ref.String() == "" && nm != gnm { 725 addExtension(&ps.VendorExtensible, "x-go-name", gnm) 726 } 727 // we have 2 cases: 728 // 1. field with different name override tag 729 // 2. field with different name removes tag 730 // so we need to save both tag&name 731 seenProperties[nm] = gnm 732 schema.Properties[nm] = ps 733 } 734 } 735 if schema != nil && hasAllOf && len(schema.Properties) > 0 { 736 bschema.AllOf = append(bschema.AllOf, *schema) 737 } 738 for k := range schema.Properties { 739 if _, ok := seenProperties[k]; !ok { 740 delete(schema.Properties, k) 741 } 742 } 743 return nil 744 } 745 746 var schemaVendorExtensibleParser = vendorExtensibleParser{ 747 setExtensions: func(ext spec.Extensions, dest interface{}) { 748 dest.(*spec.Schema).Extensions = ext 749 }, 750 } 751 752 func (scp *schemaParser) createParser(nm string, schema, ps *spec.Schema, fld *ast.Field) *sectionedParser { 753 sp := new(sectionedParser) 754 755 schemeType, err := ps.Type.MarshalJSON() 756 if err != nil { 757 return nil 758 } 759 760 if ps.Ref.String() == "" { 761 sp.setDescription = func(lines []string) { ps.Description = joinDropLast(lines) } 762 sp.taggers = []tagParser{ 763 newSingleLineTagParser("maximum", &setMaximum{schemaValidations{ps}, rxf(rxMaximumFmt, "")}), 764 newSingleLineTagParser("minimum", &setMinimum{schemaValidations{ps}, rxf(rxMinimumFmt, "")}), 765 newSingleLineTagParser("multipleOf", &setMultipleOf{schemaValidations{ps}, rxf(rxMultipleOfFmt, "")}), 766 newSingleLineTagParser("minLength", &setMinLength{schemaValidations{ps}, rxf(rxMinLengthFmt, "")}), 767 newSingleLineTagParser("maxLength", &setMaxLength{schemaValidations{ps}, rxf(rxMaxLengthFmt, "")}), 768 newSingleLineTagParser("pattern", &setPattern{schemaValidations{ps}, rxf(rxPatternFmt, "")}), 769 newSingleLineTagParser("minItems", &setMinItems{schemaValidations{ps}, rxf(rxMinItemsFmt, "")}), 770 newSingleLineTagParser("maxItems", &setMaxItems{schemaValidations{ps}, rxf(rxMaxItemsFmt, "")}), 771 newSingleLineTagParser("unique", &setUnique{schemaValidations{ps}, rxf(rxUniqueFmt, "")}), 772 newSingleLineTagParser("enum", &setEnum{schemaValidations{ps}, rxf(rxEnumFmt, "")}), 773 newSingleLineTagParser("default", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}), 774 newSingleLineTagParser("type", &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxDefaultFmt, "")}), 775 newSingleLineTagParser("example", &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{ps}, rxf(rxExampleFmt, "")}), 776 newSingleLineTagParser("required", &setRequiredSchema{schema, nm}), 777 newSingleLineTagParser("readOnly", &setReadOnlySchema{ps}), 778 newSingleLineTagParser("discriminator", &setDiscriminator{schema, nm}), 779 newMultiLineTagParser("YAMLExtensionsBlock", newYamlParser(rxExtensions, schemaVendorExtensibleParser.ParseInto(ps)), true), 780 } 781 782 itemsTaggers := func(items *spec.Schema, level int) []tagParser { 783 schemeType, err := items.Type.MarshalJSON() 784 if err != nil { 785 return nil 786 } 787 // the expression is 1-index based not 0-index 788 itemsPrefix := fmt.Sprintf(rxItemsPrefixFmt, level+1) 789 return []tagParser{ 790 newSingleLineTagParser(fmt.Sprintf("items%dMaximum", level), &setMaximum{schemaValidations{items}, rxf(rxMaximumFmt, itemsPrefix)}), 791 newSingleLineTagParser(fmt.Sprintf("items%dMinimum", level), &setMinimum{schemaValidations{items}, rxf(rxMinimumFmt, itemsPrefix)}), 792 newSingleLineTagParser(fmt.Sprintf("items%dMultipleOf", level), &setMultipleOf{schemaValidations{items}, rxf(rxMultipleOfFmt, itemsPrefix)}), 793 newSingleLineTagParser(fmt.Sprintf("items%dMinLength", level), &setMinLength{schemaValidations{items}, rxf(rxMinLengthFmt, itemsPrefix)}), 794 newSingleLineTagParser(fmt.Sprintf("items%dMaxLength", level), &setMaxLength{schemaValidations{items}, rxf(rxMaxLengthFmt, itemsPrefix)}), 795 newSingleLineTagParser(fmt.Sprintf("items%dPattern", level), &setPattern{schemaValidations{items}, rxf(rxPatternFmt, itemsPrefix)}), 796 newSingleLineTagParser(fmt.Sprintf("items%dMinItems", level), &setMinItems{schemaValidations{items}, rxf(rxMinItemsFmt, itemsPrefix)}), 797 newSingleLineTagParser(fmt.Sprintf("items%dMaxItems", level), &setMaxItems{schemaValidations{items}, rxf(rxMaxItemsFmt, itemsPrefix)}), 798 newSingleLineTagParser(fmt.Sprintf("items%dUnique", level), &setUnique{schemaValidations{items}, rxf(rxUniqueFmt, itemsPrefix)}), 799 newSingleLineTagParser(fmt.Sprintf("items%dEnum", level), &setEnum{schemaValidations{items}, rxf(rxEnumFmt, itemsPrefix)}), 800 newSingleLineTagParser(fmt.Sprintf("items%dDefault", level), &setDefault{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxDefaultFmt, itemsPrefix)}), 801 newSingleLineTagParser(fmt.Sprintf("items%dExample", level), &setExample{&spec.SimpleSchema{Type: string(schemeType)}, schemaValidations{items}, rxf(rxExampleFmt, itemsPrefix)}), 802 } 803 } 804 805 var parseArrayTypes func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error) 806 parseArrayTypes = func(expr ast.Expr, items *spec.SchemaOrArray, level int) ([]tagParser, error) { 807 if items == nil || items.Schema == nil { 808 return []tagParser{}, nil 809 } 810 switch iftpe := expr.(type) { 811 case *ast.ArrayType: 812 eleTaggers := itemsTaggers(items.Schema, level) 813 sp.taggers = append(eleTaggers, sp.taggers...) 814 otherTaggers, err := parseArrayTypes(iftpe.Elt, items.Schema.Items, level+1) 815 if err != nil { 816 return nil, err 817 } 818 return otherTaggers, nil 819 case *ast.Ident: 820 taggers := []tagParser{} 821 if iftpe.Obj == nil { 822 taggers = itemsTaggers(items.Schema, level) 823 } 824 otherTaggers, err := parseArrayTypes(expr, items.Schema.Items, level+1) 825 if err != nil { 826 return nil, err 827 } 828 return append(taggers, otherTaggers...), nil 829 case *ast.StarExpr: 830 otherTaggers, err := parseArrayTypes(iftpe.X, items, level) 831 if err != nil { 832 return nil, err 833 } 834 return otherTaggers, nil 835 default: 836 return nil, fmt.Errorf("unknown field type ele for %q", nm) 837 } 838 } 839 // check if this is a primitive, if so parse the validations from the 840 // doc comments of the slice declaration. 841 if ftped, ok := fld.Type.(*ast.ArrayType); ok { 842 taggers, err := parseArrayTypes(ftped.Elt, ps.Items, 0) 843 if err != nil { 844 return sp 845 } 846 sp.taggers = append(taggers, sp.taggers...) 847 } 848 849 } else { 850 sp.taggers = []tagParser{ 851 newSingleLineTagParser("required", &setRequiredSchema{schema, nm}), 852 } 853 } 854 return sp 855 } 856 857 // hasFilePathPrefix reports whether the filesystem path s begins with the 858 // elements in prefix. 859 // 860 // taken from: https://github.com/golang/go/blob/c87520c5981ecdeaa99e7ba636a6088f900c0c75/src/cmd/go/internal/load/path.go#L60-L80 861 func hasFilePathPrefix(s, prefix string) bool { 862 sv := strings.ToUpper(filepath.VolumeName(s)) 863 pv := strings.ToUpper(filepath.VolumeName(prefix)) 864 s = s[len(sv):] 865 prefix = prefix[len(pv):] 866 switch { 867 default: 868 return false 869 case sv != pv: 870 return false 871 case len(s) == len(prefix): 872 return s == prefix 873 case len(s) > len(prefix): 874 if prefix != "" && prefix[len(prefix)-1] == filepath.Separator { 875 return strings.HasPrefix(s, prefix) 876 } 877 return s[len(prefix)] == filepath.Separator && s[:len(prefix)] == prefix 878 } 879 } 880 881 func (scp *schemaParser) packageForFile(gofile *ast.File, tpe *ast.Ident) (*loader.PackageInfo, error) { 882 fn := scp.program.Fset.File(gofile.Pos()).Name() 883 if Debug { 884 log.Println("trying for", fn, tpe.Name, tpe.String()) 885 } 886 fa, err := filepath.Abs(fn) 887 if err != nil { 888 return nil, err 889 } 890 if Debug { 891 log.Println("absolute path", fa) 892 } 893 var fgp string 894 gopath := os.Getenv("GOPATH") 895 if gopath == "" { 896 gopath = filepath.Join(os.Getenv("HOME"), "go") 897 } 898 for _, p := range append(filepath.SplitList(gopath), runtime.GOROOT()) { 899 pref := filepath.Join(p, "src") 900 if hasFilePathPrefix(fa, pref) { 901 fgp = filepath.Dir(strings.TrimPrefix(fa, pref))[1:] 902 break 903 } 904 } 905 if Debug { 906 log.Println("package in gopath", fgp) 907 } 908 for pkg, pkgInfo := range scp.program.AllPackages { 909 if Debug { 910 log.Println("inferring for", tpe.Name, "with", gofile.Name.Name, "at", pkg.Path(), "against", filepath.ToSlash(fgp)) 911 } 912 if pkg.Name() == gofile.Name.Name && filepath.ToSlash(fgp) == pkg.Path() { 913 return pkgInfo, nil 914 } 915 } 916 917 return nil, fmt.Errorf("unable to determine package for %s", fn) 918 } 919 920 func (scp *schemaParser) packageForSelector(gofile *ast.File, expr ast.Expr) (*loader.PackageInfo, error) { 921 922 if pth, ok := expr.(*ast.Ident); ok { 923 // lookup import 924 var selPath string 925 for _, imp := range gofile.Imports { 926 pv, err := strconv.Unquote(imp.Path.Value) 927 if err != nil { 928 pv = imp.Path.Value 929 } 930 if imp.Name != nil { 931 if imp.Name.Name == pth.Name { 932 selPath = pv 933 break 934 } 935 } else { 936 pkg := scp.program.Package(pv) 937 if pkg != nil && pth.Name == pkg.Pkg.Name() { 938 selPath = pv 939 break 940 } else { 941 parts := strings.Split(pv, "/") 942 if len(parts) > 0 && parts[len(parts)-1] == pth.Name { 943 selPath = pv 944 break 945 } 946 } 947 } 948 } 949 // find actual struct 950 if selPath == "" { 951 return nil, fmt.Errorf("no import found for %s", pth.Name) 952 } 953 954 pkg := scp.program.Package(selPath) 955 if pkg != nil { 956 return pkg, nil 957 } 958 // TODO: I must admit this made me cry, it's not even a great solution. 959 pkg = scp.program.Package("github.com/jamescostian/go-swagger/vendor/" + selPath) 960 if pkg != nil { 961 return pkg, nil 962 } 963 for _, info := range scp.program.AllPackages { 964 n := info.String() 965 path := "/vendor/" + selPath 966 if strings.HasSuffix(n, path) { 967 pkg = scp.program.Package(n) 968 return pkg, nil 969 } 970 } 971 } 972 return nil, fmt.Errorf("can't determine selector path from %v", expr) 973 } 974 975 func (scp *schemaParser) makeRef(file *ast.File, pkg *loader.PackageInfo, gd *ast.GenDecl, ts *ast.TypeSpec, prop swaggerTypable) error { 976 sd := newSchemaDecl(file, gd, ts) 977 sd.inferNames() 978 // make an exception for time.Time because this is a well-known string format 979 if sd.Name == "Time" && pkg.String() == "time" { 980 return nil 981 } 982 ref, err := spec.NewRef("#/definitions/" + sd.Name) 983 if err != nil { 984 return err 985 } 986 prop.SetRef(ref) 987 scp.postDecls = append(scp.postDecls, *sd) 988 return nil 989 } 990 991 func (scp *schemaParser) parseIdentProperty(pkg *loader.PackageInfo, expr *ast.Ident, prop swaggerTypable) error { 992 // before proceeding make an exception to time.Time because it is a well known string format 993 if pkg.String() == "time" && expr.String() == "Time" { 994 prop.Typed("string", "date-time") 995 return nil 996 } 997 998 // find the file this selector points to 999 file, gd, ts, err := findSourceFile(pkg, expr.Name) 1000 1001 if err != nil { 1002 err := swaggerSchemaForType(expr.Name, prop) 1003 if err != nil { 1004 return fmt.Errorf("package %s, error is: %v", pkg.String(), err) 1005 } 1006 return nil 1007 } 1008 1009 if at, ok := ts.Type.(*ast.ArrayType); ok { 1010 // the swagger spec defines strfmt base64 as []byte. 1011 // in that case we don't actually want to turn it into an array 1012 // but we want to turn it into a string 1013 if _, ok := at.Elt.(*ast.Ident); ok { 1014 if strfmtName, ok := strfmtName(gd.Doc); ok { 1015 prop.Typed("string", strfmtName) 1016 return nil 1017 } 1018 } 1019 // this is a selector, so most likely not base64 1020 if strfmtName, ok := strfmtName(gd.Doc); ok { 1021 prop.Items().Typed("string", strfmtName) 1022 return nil 1023 } 1024 } 1025 1026 // look at doc comments for swagger:strfmt [name] 1027 // when found this is the format name, create a schema with that name 1028 if strfmtName, ok := strfmtName(gd.Doc); ok { 1029 prop.Typed("string", strfmtName) 1030 return nil 1031 } 1032 1033 if enumName, ok := enumName(gd.Doc); ok { 1034 var enumValues = getEnumValues(file, enumName) 1035 if len(enumValues) > 0 { 1036 prop.WithEnum(enumValues...) 1037 var typeName = reflect.TypeOf(enumValues[0]).String() 1038 err := swaggerSchemaForType(typeName, prop) 1039 if err != nil { 1040 return fmt.Errorf("file %s, error is: %v", file.Name, err) 1041 } 1042 } 1043 } 1044 1045 if defaultName, ok := defaultName(gd.Doc); ok { 1046 log.Println(defaultName) 1047 return nil 1048 } 1049 1050 if typeName, ok := typeName(gd.Doc); ok { 1051 _ = swaggerSchemaForType(typeName, prop) 1052 return nil 1053 } 1054 1055 if isAliasParam(prop) || aliasParam(gd.Doc) { 1056 itype, ok := ts.Type.(*ast.Ident) 1057 if ok { 1058 err := swaggerSchemaForType(itype.Name, prop) 1059 if err == nil { 1060 return nil 1061 } 1062 } 1063 } 1064 switch tpe := ts.Type.(type) { 1065 case *ast.ArrayType: 1066 return scp.makeRef(file, pkg, gd, ts, prop) 1067 case *ast.StructType: 1068 return scp.makeRef(file, pkg, gd, ts, prop) 1069 1070 case *ast.Ident: 1071 return scp.makeRef(file, pkg, gd, ts, prop) 1072 1073 case *ast.StarExpr: 1074 return parseProperty(scp, file, tpe.X, prop) 1075 1076 case *ast.SelectorExpr: 1077 // return scp.refForSelector(file, gd, tpe, ts, prop) 1078 return scp.makeRef(file, pkg, gd, ts, prop) 1079 1080 case *ast.InterfaceType: 1081 return scp.makeRef(file, pkg, gd, ts, prop) 1082 1083 case *ast.MapType: 1084 return scp.makeRef(file, pkg, gd, ts, prop) 1085 1086 default: 1087 err := swaggerSchemaForType(expr.Name, prop) 1088 if err != nil { 1089 return fmt.Errorf("package %s, error is: %v", pkg.String(), err) 1090 } 1091 return nil 1092 } 1093 1094 } 1095 1096 func (scp *schemaParser) typeForSelector(gofile *ast.File, expr *ast.SelectorExpr, prop swaggerTypable) error { 1097 pkg, err := scp.packageForSelector(gofile, expr.X) 1098 if err != nil { 1099 return err 1100 } 1101 1102 return scp.parseIdentProperty(pkg, expr.Sel, prop) 1103 } 1104 1105 func findSourceFile(pkg *loader.PackageInfo, typeName string) (*ast.File, *ast.GenDecl, *ast.TypeSpec, error) { 1106 for _, file := range pkg.Files { 1107 for _, decl := range file.Decls { 1108 if gd, ok := decl.(*ast.GenDecl); ok { 1109 for _, gs := range gd.Specs { 1110 if ts, ok := gs.(*ast.TypeSpec); ok { 1111 strfmtNme, isStrfmt := strfmtName(gd.Doc) 1112 if (isStrfmt && strfmtNme == typeName) || ts.Name != nil && ts.Name.Name == typeName { 1113 return file, gd, ts, nil 1114 } 1115 } 1116 } 1117 } 1118 } 1119 } 1120 return nil, nil, nil, fmt.Errorf("unable to find %s in %s", typeName, pkg.String()) 1121 } 1122 1123 func allOfMember(comments *ast.CommentGroup) bool { 1124 if comments != nil { 1125 for _, cmt := range comments.List { 1126 for _, ln := range strings.Split(cmt.Text, "\n") { 1127 if rxAllOf.MatchString(ln) { 1128 return true 1129 } 1130 } 1131 } 1132 } 1133 return false 1134 } 1135 1136 func fileParam(comments *ast.CommentGroup) bool { 1137 if comments != nil { 1138 for _, cmt := range comments.List { 1139 for _, ln := range strings.Split(cmt.Text, "\n") { 1140 if rxFileUpload.MatchString(ln) { 1141 return true 1142 } 1143 } 1144 } 1145 } 1146 return false 1147 } 1148 1149 func strfmtName(comments *ast.CommentGroup) (string, bool) { 1150 if comments != nil { 1151 for _, cmt := range comments.List { 1152 for _, ln := range strings.Split(cmt.Text, "\n") { 1153 matches := rxStrFmt.FindStringSubmatch(ln) 1154 if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { 1155 return strings.TrimSpace(matches[1]), true 1156 } 1157 } 1158 } 1159 } 1160 return "", false 1161 } 1162 1163 func ignored(comments *ast.CommentGroup) bool { 1164 if comments != nil { 1165 for _, cmt := range comments.List { 1166 for _, ln := range strings.Split(cmt.Text, "\n") { 1167 if rxIgnoreOverride.MatchString(ln) { 1168 return true 1169 } 1170 } 1171 } 1172 } 1173 return false 1174 } 1175 1176 func enumName(comments *ast.CommentGroup) (string, bool) { 1177 if comments != nil { 1178 for _, cmt := range comments.List { 1179 for _, ln := range strings.Split(cmt.Text, "\n") { 1180 matches := rxEnum.FindStringSubmatch(ln) 1181 if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { 1182 return strings.TrimSpace(matches[1]), true 1183 } 1184 } 1185 } 1186 } 1187 return "", false 1188 } 1189 1190 func aliasParam(comments *ast.CommentGroup) bool { 1191 if comments != nil { 1192 for _, cmt := range comments.List { 1193 for _, ln := range strings.Split(cmt.Text, "\n") { 1194 if rxAlias.MatchString(ln) { 1195 return true 1196 } 1197 } 1198 } 1199 } 1200 return false 1201 } 1202 1203 func defaultName(comments *ast.CommentGroup) (string, bool) { 1204 if comments != nil { 1205 for _, cmt := range comments.List { 1206 for _, ln := range strings.Split(cmt.Text, "\n") { 1207 matches := rxDefault.FindStringSubmatch(ln) 1208 if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { 1209 return strings.TrimSpace(matches[1]), true 1210 } 1211 } 1212 } 1213 } 1214 return "", false 1215 } 1216 1217 func typeName(comments *ast.CommentGroup) (string, bool) { 1218 1219 var typ string 1220 if comments != nil { 1221 for _, cmt := range comments.List { 1222 for _, ln := range strings.Split(cmt.Text, "\n") { 1223 matches := rxType.FindStringSubmatch(ln) 1224 if len(matches) > 1 && len(strings.TrimSpace(matches[1])) > 0 { 1225 typ = strings.TrimSpace(matches[1]) 1226 return typ, true 1227 } 1228 } 1229 } 1230 } 1231 return "", false 1232 } 1233 1234 func parseProperty(scp *schemaParser, gofile *ast.File, fld ast.Expr, prop swaggerTypable) error { 1235 switch ftpe := fld.(type) { 1236 case *ast.Ident: // simple value 1237 pkg, err := scp.packageForFile(gofile, ftpe) 1238 if err != nil { 1239 return err 1240 } 1241 return scp.parseIdentProperty(pkg, ftpe, prop) 1242 1243 case *ast.StarExpr: // pointer to something, optional by default 1244 if err := parseProperty(scp, gofile, ftpe.X, prop); err != nil { 1245 return err 1246 } 1247 1248 case *ast.ArrayType: // slice type 1249 if err := parseProperty(scp, gofile, ftpe.Elt, prop.Items()); err != nil { 1250 return err 1251 } 1252 1253 case *ast.StructType: 1254 schema := prop.Schema() 1255 if schema == nil { 1256 return fmt.Errorf("items doesn't support embedded structs") 1257 } 1258 return scp.parseStructType(gofile, prop.Schema(), ftpe, make(map[string]string)) 1259 1260 case *ast.SelectorExpr: 1261 err := scp.typeForSelector(gofile, ftpe, prop) 1262 return err 1263 1264 case *ast.MapType: 1265 // check if key is a string type, if not print a message 1266 // and skip the map property. Only maps with string keys can go into additional properties 1267 sch := prop.Schema() 1268 if sch == nil { 1269 return fmt.Errorf("items doesn't support maps") 1270 } 1271 if keyIdent, ok := ftpe.Key.(*ast.Ident); sch != nil && ok { 1272 if keyIdent.Name == "string" { 1273 if sch.AdditionalProperties == nil { 1274 sch.AdditionalProperties = new(spec.SchemaOrBool) 1275 } 1276 sch.AdditionalProperties.Allows = false 1277 if sch.AdditionalProperties.Schema == nil { 1278 sch.AdditionalProperties.Schema = new(spec.Schema) 1279 } 1280 if err := parseProperty(scp, gofile, ftpe.Value, schemaTypable{sch.AdditionalProperties.Schema, 0}); err != nil { 1281 return err 1282 } 1283 sch.Typed("object", "") 1284 } 1285 } 1286 1287 case *ast.InterfaceType: 1288 prop.Schema().Typed("object", "") 1289 default: 1290 pos := "unknown file:unknown position" 1291 if scp != nil { 1292 if scp.program != nil { 1293 if scp.program.Fset != nil { 1294 pos = scp.program.Fset.Position(fld.Pos()).String() 1295 } 1296 } 1297 } 1298 return fmt.Errorf("Expr (%s) is unsupported for a schema", pos) 1299 } 1300 return nil 1301 } 1302 1303 func parseJSONTag(field *ast.Field) (name string, ignore bool, isString bool, err error) { 1304 if len(field.Names) > 0 { 1305 name = field.Names[0].Name 1306 } 1307 if field.Tag != nil && len(strings.TrimSpace(field.Tag.Value)) > 0 { 1308 tv, err := strconv.Unquote(field.Tag.Value) 1309 if err != nil { 1310 return name, false, false, err 1311 } 1312 1313 if strings.TrimSpace(tv) != "" { 1314 st := reflect.StructTag(tv) 1315 jsonParts := strings.Split(st.Get("json"), ",") 1316 jsonName := jsonParts[0] 1317 1318 if len(jsonParts) > 1 && jsonParts[1] == "string" { 1319 isString = isFieldStringable(field.Type) 1320 } 1321 1322 if jsonName == "-" { 1323 return name, true, isString, nil 1324 } else if jsonName != "" { 1325 return jsonName, false, isString, nil 1326 } 1327 } 1328 } 1329 return name, false, false, nil 1330 } 1331 1332 // isFieldStringable check if the field type is a scalar. If the field type is 1333 // *ast.StarExpr and is pointer type, check if it refers to a scalar. 1334 // Otherwise, the ",string" directive doesn't apply. 1335 func isFieldStringable(tpe ast.Expr) bool { 1336 if ident, ok := tpe.(*ast.Ident); ok { 1337 switch ident.Name { 1338 case "int", "int8", "int16", "int32", "int64", 1339 "uint", "uint8", "uint16", "uint32", "uint64", 1340 "float64", "string", "bool": 1341 return true 1342 } 1343 } else if starExpr, ok := tpe.(*ast.StarExpr); ok { 1344 return isFieldStringable(starExpr.X) 1345 } else { 1346 return false 1347 } 1348 return false 1349 }