1// Copyright 2015 go-swagger maintainers 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package generator 16 17import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "log" 23 "os" 24 "path/filepath" 25 "reflect" 26 "sort" 27 "strings" 28 "text/template" 29 30 swaggererrors "github.com/go-openapi/errors" 31 32 "github.com/go-openapi/analysis" 33 "github.com/go-openapi/validate" 34 "github.com/go-openapi/strfmt" 35 "github.com/go-openapi/loads" 36 "github.com/go-openapi/spec" 37 "github.com/go-openapi/swag" 38 "golang.org/x/tools/imports" 39) 40 41//go:generate go-bindata -mode 420 -modtime 1482416923 -pkg=generator -ignore=.*\.sw? ./templates/... 42 43// LanguageOpts to describe a language to the code generator 44type LanguageOpts struct { 45 ReservedWords []string 46 reservedWordsSet map[string]struct{} 47 initialized bool 48 formatFunc func(string, []byte) ([]byte, error) 49} 50 51// Init the language option 52func (l *LanguageOpts) Init() { 53 if !l.initialized { 54 l.initialized = true 55 l.reservedWordsSet = make(map[string]struct{}) 56 for _, rw := range l.ReservedWords { 57 l.reservedWordsSet[rw] = struct{}{} 58 } 59 } 60} 61 62// MangleName makes sure a reserved word gets a safe name 63func (l *LanguageOpts) MangleName(name, suffix string) string { 64 if _, ok := l.reservedWordsSet[swag.ToFileName(name)]; !ok { 65 return name 66 } 67 return strings.Join([]string{name, suffix}, "_") 68} 69 70// MangleVarName makes sure a reserved word gets a safe name 71func (l *LanguageOpts) MangleVarName(name string) string { 72 nm := swag.ToVarName(name) 73 if _, ok := l.reservedWordsSet[nm]; !ok { 74 return nm 75 } 76 return nm + "Var" 77} 78 79// FormatContent formats a file with a language specific formatter 80func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) { 81 if l.formatFunc != nil { 82 return l.formatFunc(name, content) 83 } 84 return content, nil 85} 86 87var golang = GoLangOpts() 88 89// GoLangOpts for rendering items as golang code 90func GoLangOpts() *LanguageOpts { 91 opts := new(LanguageOpts) 92 opts.ReservedWords = []string{ 93 "break", "default", "func", "interface", "select", 94 "case", "defer", "go", "map", "struct", 95 "chan", "else", "goto", "package", "switch", 96 "const", "fallthrough", "if", "range", "type", 97 "continue", "for", "import", "return", "var", 98 } 99 opts.formatFunc = func(ffn string, content []byte) ([]byte, error) { 100 opts := new(imports.Options) 101 opts.TabIndent = true 102 opts.TabWidth = 2 103 opts.Fragment = true 104 opts.Comments = true 105 return imports.Process(ffn, content, opts) 106 } 107 opts.Init() 108 return opts 109} 110 111// Debug when the env var DEBUG is not empty 112// the generators will be very noisy about what they are doing 113var Debug = os.Getenv("DEBUG") != "" 114 115func findSwaggerSpec(nm string) (string, error) { 116 specs := []string{"swagger.json", "swagger.yml", "swagger.yaml"} 117 if nm != "" { 118 specs = []string{nm} 119 } 120 var name string 121 for _, nn := range specs { 122 f, err := os.Stat(nn) 123 if err != nil && !os.IsNotExist(err) { 124 return "", err 125 } 126 if err != nil && os.IsNotExist(err) { 127 continue 128 } 129 if f.IsDir() { 130 return "", fmt.Errorf("%s is a directory", nn) 131 } 132 name = nn 133 break 134 } 135 if name == "" { 136 return "", errors.New("couldn't find a swagger spec") 137 } 138 return name, nil 139} 140 141// DefaultSectionOpts for a given opts, this is used when no config file is passed 142// and uses the embedded templates when no local override can be found 143func DefaultSectionOpts(gen *GenOpts, client bool) { 144 sec := gen.Sections 145 if len(sec.Models) == 0 { 146 sec.Models = []TemplateOpts{ 147 { 148 Name: "definition", 149 Source: "asset:model", 150 Target: "{{ joinFilePath .Target .ModelPackage }}", 151 FileName: "{{ (snakize (pascalize .Name)) }}.go", 152 }, 153 } 154 } 155 156 if len(sec.Operations) == 0 { 157 if client { 158 sec.Operations = []TemplateOpts{ 159 { 160 Name: "parameters", 161 Source: "asset:clientParameter", 162 Target: "{{ joinFilePath .Target .ClientPackage .Package }}", 163 FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", 164 }, 165 { 166 Name: "responses", 167 Source: "asset:clientResponse", 168 Target: "{{ joinFilePath .Target .ClientPackage .Package }}", 169 FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", 170 }, 171 } 172 173 } else { 174 ops := []TemplateOpts{} 175 if gen.IncludeParameters { 176 ops = append(ops, TemplateOpts{ 177 Name: "parameters", 178 Source: "asset:serverParameter", 179 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}", 180 FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", 181 }) 182 } 183 if gen.IncludeURLBuilder { 184 ops = append(ops, TemplateOpts{ 185 Name: "urlbuilder", 186 Source: "asset:serverUrlbuilder", 187 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}", 188 FileName: "{{ (snakize (pascalize .Name)) }}_urlbuilder.go", 189 }) 190 } 191 if gen.IncludeResponses { 192 ops = append(ops, TemplateOpts{ 193 Name: "responses", 194 Source: "asset:serverResponses", 195 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}", 196 FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", 197 }) 198 } 199 if gen.IncludeHandler { 200 ops = append(ops, TemplateOpts{ 201 Name: "handler", 202 Source: "asset:serverOperation", 203 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target .ServerPackage .APIPackage .Package }}{{ else }}{{ joinFilePath .Target .ServerPackage .Package }}{{ end }}", 204 FileName: "{{ (snakize (pascalize .Name)) }}.go", 205 }) 206 } 207 sec.Operations = ops 208 } 209 } 210 211 if len(sec.OperationGroups) == 0 { 212 if client { 213 sec.OperationGroups = []TemplateOpts{ 214 { 215 Name: "client", 216 Source: "asset:clientClient", 217 Target: "{{ joinFilePath .Target .ClientPackage .Name }}", 218 FileName: "{{ (snakize (pascalize .Name)) }}_client.go", 219 }, 220 } 221 } else { 222 sec.OperationGroups = []TemplateOpts{} 223 } 224 } 225 226 if len(sec.Application) == 0 { 227 if client { 228 sec.Application = []TemplateOpts{ 229 { 230 Name: "facade", 231 Source: "asset:clientFacade", 232 Target: "{{ joinFilePath .Target .ClientPackage }}", 233 FileName: "{{ .Name }}Client.go", 234 }, 235 } 236 } else { 237 sec.Application = []TemplateOpts{ 238 { 239 Name: "configure", 240 Source: "asset:serverConfigureapi", 241 Target: "{{ joinFilePath .Target .ServerPackage }}", 242 FileName: "configure_{{ (snakize (pascalize .Name)) }}.go", 243 SkipExists: true, 244 }, 245 { 246 Name: "main", 247 Source: "asset:serverMain", 248 Target: "{{ joinFilePath .Target \"cmd\" (dasherize (pascalize .Name)) }}-server", 249 FileName: "main.go", 250 }, 251 { 252 Name: "embedded_spec", 253 Source: "asset:swaggerJsonEmbed", 254 Target: "{{ joinFilePath .Target .ServerPackage }}", 255 FileName: "embedded_spec.go", 256 }, 257 { 258 Name: "server", 259 Source: "asset:serverServer", 260 Target: "{{ joinFilePath .Target .ServerPackage }}", 261 FileName: "server.go", 262 }, 263 { 264 Name: "builder", 265 Source: "asset:serverBuilder", 266 Target: "{{ joinFilePath .Target .ServerPackage .Package }}", 267 FileName: "{{ snakize (pascalize .Name) }}_api.go", 268 }, 269 { 270 Name: "doc", 271 Source: "asset:serverDoc", 272 Target: "{{ joinFilePath .Target .ServerPackage }}", 273 FileName: "doc.go", 274 }, 275 } 276 } 277 } 278 gen.Sections = sec 279 280} 281 282// TemplateOpts allows 283type TemplateOpts struct { 284 Name string `mapstructure:"name"` 285 Source string `mapstructure:"source"` 286 Target string `mapstructure:"target"` 287 FileName string `mapstructure:"file_name"` 288 SkipExists bool `mapstructure:"skip_exists"` 289 SkipFormat bool `mapstructure:"skip_format"` 290} 291 292// SectionOpts allows for specifying options to customize the templates used for generation 293type SectionOpts struct { 294 Application []TemplateOpts `mapstructure:"application"` 295 Operations []TemplateOpts `mapstructure:"operations"` 296 OperationGroups []TemplateOpts `mapstructure:"operation_groups"` 297 Models []TemplateOpts `mapstructure:"models"` 298} 299 300// GenOpts the options for the generator 301type GenOpts struct { 302 IncludeModel bool 303 IncludeValidator bool 304 IncludeHandler bool 305 IncludeParameters bool 306 IncludeResponses bool 307 IncludeURLBuilder bool 308 IncludeMain bool 309 IncludeSupport bool 310 ExcludeSpec bool 311 DumpData bool 312 WithContext bool 313 ValidateSpec bool 314 defaultsEnsured bool 315 316 Spec string 317 APIPackage string 318 ModelPackage string 319 ServerPackage string 320 ClientPackage string 321 Principal string 322 Target string 323 Sections SectionOpts 324 LanguageOpts *LanguageOpts 325 TypeMapping map[string]string 326 Imports map[string]string 327 DefaultScheme string 328 DefaultProduces string 329 DefaultConsumes string 330 TemplateDir string 331 Operations []string 332 Models []string 333 Tags []string 334 Name string 335 FlagStrategy string 336 CompatibilityMode string 337} 338 339// TargetPath returns the target path relative to the server package 340func (g *GenOpts) TargetPath() string { 341 tgtAbs, err := filepath.Abs(g.Target) 342 if err != nil { 343 log.Fatalln(err) 344 } 345 srvrAbs, err := filepath.Abs(g.ServerPackage) 346 if err != nil { 347 log.Fatalln(err) 348 } 349 tgtRel, err := filepath.Rel(srvrAbs, tgtAbs) 350 if err != nil { 351 log.Fatalln(err) 352 } 353 return tgtRel 354} 355 356// SpecPath returns the path to the spec relative to the server package 357func (g *GenOpts) SpecPath() string { 358 if strings.HasPrefix(g.Spec, "http://") || strings.HasPrefix(g.Spec, "https://") { 359 return g.Spec 360 } 361 specAbs, err := filepath.Abs(g.Spec) 362 if err != nil { 363 log.Fatalln(err) 364 } 365 srvrAbs, err := filepath.Abs(g.ServerPackage) 366 if err != nil { 367 log.Fatalln(err) 368 } 369 specRel, err := filepath.Rel(srvrAbs, specAbs) 370 if err != nil { 371 log.Fatalln(err) 372 } 373 return specRel 374} 375 376// EnsureDefaults for these gen opts 377func (g *GenOpts) EnsureDefaults(client bool) error { 378 if g.defaultsEnsured { 379 return nil 380 } 381 DefaultSectionOpts(g, client) 382 if g.LanguageOpts == nil { 383 g.LanguageOpts = GoLangOpts() 384 } 385 g.defaultsEnsured = true 386 return nil 387} 388 389func (g *GenOpts) location(t *TemplateOpts, data interface{}) (string, string, error) { 390 v := reflect.Indirect(reflect.ValueOf(data)) 391 fld := v.FieldByName("Name") 392 var name string 393 if fld.IsValid() { 394 log.Println("name field", fld.String()) 395 name = fld.String() 396 } 397 398 fldpack := v.FieldByName("Package") 399 pkg := g.APIPackage 400 if fldpack.IsValid() { 401 log.Println("package field", fldpack.String()) 402 pkg = fldpack.String() 403 } 404 405 var tags []string 406 tagsF := v.FieldByName("Tags") 407 if tagsF.IsValid() { 408 tags = tagsF.Interface().([]string) 409 } 410 411 pthTpl, err := template.New(t.Name + "-target").Funcs(FuncMap).Parse(t.Target) 412 if err != nil { 413 return "", "", err 414 } 415 416 fNameTpl, err := template.New(t.Name + "-filename").Funcs(FuncMap).Parse(t.FileName) 417 if err != nil { 418 return "", "", err 419 } 420 421 d := struct { 422 Name, Package, APIPackage, ServerPackage, ClientPackage, ModelPackage, Target string 423 Tags []string 424 }{ 425 Name: name, 426 Package: pkg, 427 APIPackage: g.APIPackage, 428 ServerPackage: g.ServerPackage, 429 ClientPackage: g.ClientPackage, 430 ModelPackage: g.ModelPackage, 431 Target: g.Target, 432 Tags: tags, 433 } 434 435 // pretty.Println(data) 436 var pthBuf bytes.Buffer 437 if e := pthTpl.Execute(&pthBuf, d); e != nil { 438 return "", "", e 439 } 440 441 var fNameBuf bytes.Buffer 442 if e := fNameTpl.Execute(&fNameBuf, d); e != nil { 443 return "", "", e 444 } 445 return pthBuf.String(), fileName(fNameBuf.String()), nil 446} 447 448func (g *GenOpts) render(t *TemplateOpts, data interface{}) ([]byte, error) { 449 var templ *template.Template 450 if strings.HasPrefix(strings.ToLower(t.Source), "asset:") { 451 tt, err := templates.Get(strings.TrimPrefix(t.Source, "asset:")) 452 if err != nil { 453 return nil, err 454 } 455 templ = tt 456 } 457 458 if templ == nil { 459 // try to load template from disk 460 content, err := ioutil.ReadFile(t.Source) 461 if err != nil { 462 return nil, err 463 } 464 tt, err := template.New(t.Source).Funcs(FuncMap).Parse(string(content)) 465 if err != nil { 466 return nil, err 467 } 468 templ = tt 469 } 470 if templ == nil { 471 return nil, fmt.Errorf("template %q not found", t.Source) 472 } 473 474 var tBuf bytes.Buffer 475 if err := templ.Execute(&tBuf, data); err != nil { 476 return nil, err 477 } 478 479 return tBuf.Bytes(), nil 480} 481 482func (g *GenOpts) write(t *TemplateOpts, data interface{}) error { 483 dir, fname, err := g.location(t, data) 484 if err != nil { 485 return err 486 } 487 488 if t.SkipExists && fileExists(dir, fname) { 489 log.Printf("skipping %s because it already exists", filepath.Join(dir, fname)) 490 return nil 491 } 492 493 log.Printf("creating %q in %q as %s", fname, dir, t.Name) 494 content, err := g.render(t, data) 495 if err != nil { 496 return err 497 } 498 499 if dir != "" { 500 if Debug { 501 log.Printf("skipping creating directory %q for %s because it's an empty string", dir, t.Name) 502 } 503 if e := os.MkdirAll(dir, 0700); e != nil { 504 return e 505 } 506 } 507 508 // Conditionally format the code, unless the user wants to skip 509 formatted := content 510 if t.SkipFormat == false { 511 formatted, err = g.LanguageOpts.FormatContent(fname, content) 512 if err != nil { 513 err = fmt.Errorf("format %q failed: %v", t.Name, err) 514 } 515 } 516 517 writeerr := ioutil.WriteFile(filepath.Join(dir, fname), formatted, 0644) 518 if writeerr != nil { 519 log.Printf("Failed to write %q: %s", fname, writeerr) 520 } 521 return err 522} 523 524func fileName(in string) string { 525 ext := filepath.Ext(in) 526 return swag.ToFileName(strings.TrimSuffix(in, ext)) + ext 527} 528 529func (g *GenOpts) shouldRenderApp(t *TemplateOpts, app *GenApp) bool { 530 switch swag.ToFileName(swag.ToGoName(t.Name)) { 531 case "main": 532 return g.IncludeMain 533 case "embedded_spec": 534 return !g.ExcludeSpec 535 default: 536 return true 537 } 538} 539 540func (g *GenOpts) shouldRenderOperations() bool { 541 return g.IncludeHandler || g.IncludeParameters || g.IncludeResponses 542} 543 544func (g *GenOpts) renderApplication(app *GenApp) error { 545 log.Printf("rendering %d templates for application %s", len(g.Sections.Application), app.Name) 546 for _, templ := range g.Sections.Application { 547 if !g.shouldRenderApp(&templ, app) { 548 continue 549 } 550 if err := g.write(&templ, app); err != nil { 551 return err 552 } 553 } 554 return nil 555} 556 557func (g *GenOpts) renderOperationGroup(gg *GenOperationGroup) error { 558 log.Printf("rendering %d templates for operation group %s", len(g.Sections.OperationGroups), g.Name) 559 for _, templ := range g.Sections.OperationGroups { 560 if !g.shouldRenderOperations() { 561 continue 562 } 563 564 if err := g.write(&templ, gg); err != nil { 565 return err 566 } 567 } 568 return nil 569} 570 571func (g *GenOpts) renderOperation(gg *GenOperation) error { 572 log.Printf("rendering %d templates for operation %s", len(g.Sections.Operations), g.Name) 573 for _, templ := range g.Sections.Operations { 574 if !g.shouldRenderOperations() { 575 continue 576 } 577 578 if err := g.write(&templ, gg); err != nil { 579 return err 580 } 581 } 582 return nil 583} 584 585func (g *GenOpts) renderDefinition(gg *GenDefinition) error { 586 log.Printf("rendering %d templates for model %s", len(g.Sections.Models), gg.Name) 587 for _, templ := range g.Sections.Models { 588 if !g.IncludeModel { 589 continue 590 } 591 592 if err := g.write(&templ, gg); err != nil { 593 return err 594 } 595 } 596 return nil 597} 598 599func validateSpec(path string, doc *loads.Document) (err error) { 600 if doc == nil { 601 if path, doc, err = loadSpec(path); err != nil { 602 return err 603 } 604 } 605 606 result := validate.Spec(doc, strfmt.Default) 607 if result == nil { 608 return nil 609 } 610 611 str := fmt.Sprintf("The swagger spec at %q is invalid against swagger specification %s. see errors :\n", path, doc.Version()) 612 for _, desc := range result.(*swaggererrors.CompositeError).Errors { 613 str += fmt.Sprintf("- %s\n", desc) 614 } 615 return errors.New(str) 616} 617 618func loadSpec(specFile string) (string, *loads.Document, error) { 619 // find swagger spec document, verify it exists 620 specPath := specFile 621 var err error 622 if !strings.HasPrefix(specPath, "http") { 623 specPath, err = findSwaggerSpec(specFile) 624 if err != nil { 625 return "", nil, err 626 } 627 } 628 629 // load swagger spec 630 specDoc, err := loads.Spec(specPath) 631 if err != nil { 632 return "", nil, err 633 } 634 return specPath, specDoc, nil 635} 636 637func fileExists(target, name string) bool { 638 _, err := os.Stat(filepath.Join(target, name)) 639 return !os.IsNotExist(err) 640} 641 642func gatherModels(specDoc *loads.Document, modelNames []string) (map[string]spec.Schema, error) { 643 models, mnc := make(map[string]spec.Schema), len(modelNames) 644 defs := specDoc.Spec().Definitions 645 646 if mnc > 0 { 647 var unknownModels []string 648 for _, m := range modelNames { 649 _, ok := defs[m] 650 if !ok { 651 unknownModels = append(unknownModels, m) 652 } 653 } 654 if len(unknownModels) != 0 { 655 return nil, fmt.Errorf("unknown models: %s", strings.Join(unknownModels, ", ")) 656 } 657 } 658 for k, v := range defs { 659 if mnc == 0 { 660 models[k] = v 661 } 662 for _, nm := range modelNames { 663 if k == nm { 664 models[k] = v 665 } 666 } 667 } 668 return models, nil 669} 670 671func appNameOrDefault(specDoc *loads.Document, name, defaultName string) string { 672 if strings.TrimSpace(name) == "" { 673 if specDoc.Spec().Info != nil && strings.TrimSpace(specDoc.Spec().Info.Title) != "" { 674 name = specDoc.Spec().Info.Title 675 } else { 676 name = defaultName 677 } 678 } 679 return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(swag.ToGoName(name), "Test"), "API"), "Test") 680} 681 682func containsString(names []string, name string) bool { 683 for _, nm := range names { 684 if nm == name { 685 return true 686 } 687 } 688 return false 689} 690 691type opRef struct { 692 Method string 693 Path string 694 Key string 695 ID string 696 Op *spec.Operation 697} 698 699type opRefs []opRef 700 701func (o opRefs) Len() int { return len(o) } 702func (o opRefs) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 703func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key } 704 705func gatherOperations(specDoc *analysis.Spec, operationIDs []string) map[string]opRef { 706 var oprefs opRefs 707 708 for method, pathItem := range specDoc.Operations() { 709 for path, operation := range pathItem { 710 // nm := ensureUniqueName(operation.ID, method, path, operations) 711 vv := *operation 712 oprefs = append(oprefs, opRef{ 713 Key: swag.ToGoName(strings.ToLower(method) + " " + path), 714 Method: method, 715 Path: path, 716 ID: vv.ID, 717 Op: &vv, 718 }) 719 } 720 } 721 722 sort.Sort(oprefs) 723 724 operations := make(map[string]opRef) 725 for _, opr := range oprefs { 726 nm := opr.ID 727 if nm == "" { 728 nm = opr.Key 729 } 730 731 oo, found := operations[nm] 732 if found && oo.Method != opr.Method && oo.Path != opr.Path { 733 nm = opr.Key 734 } 735 if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) { 736 opr.ID = nm 737 opr.Op.ID = nm 738 operations[nm] = opr 739 } 740 } 741 742 return operations 743} 744 745func pascalize(arg string) string { 746 if len(arg) == 0 || arg[0] > '9' { 747 return swag.ToGoName(arg) 748 } 749 if arg[0] == '+' { 750 return swag.ToGoName("Plus " + arg[1:]) 751 } 752 if arg[0] == '-' { 753 return swag.ToGoName("Minus " + arg[1:]) 754 } 755 756 return swag.ToGoName("Nr " + arg) 757} 758 759func pruneEmpty(in []string) (out []string) { 760 for _, v := range in { 761 if v != "" { 762 out = append(out, v) 763 } 764 } 765 return 766} 767 768func trimBOM(in string) string { 769 return strings.Trim(in, "\xef\xbb\xbf") 770} 771