github.com/emreu/go-swagger@v0.22.1/generator/shared.go (about) 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 15 package generator 16 17 import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "io/ioutil" 22 "log" 23 "os" 24 "path" 25 "path/filepath" 26 "reflect" 27 "regexp" 28 "sort" 29 "strings" 30 "text/template" 31 "unicode" 32 33 swaggererrors "github.com/go-openapi/errors" 34 35 "github.com/go-openapi/analysis" 36 "github.com/go-openapi/loads" 37 "github.com/go-openapi/spec" 38 "github.com/go-openapi/strfmt" 39 "github.com/go-openapi/swag" 40 "github.com/go-openapi/validate" 41 "golang.org/x/tools/imports" 42 ) 43 44 //go:generate go-bindata -mode 420 -modtime 1482416923 -pkg=generator -ignore=.*\.sw? -ignore=.*\.md ./templates/... 45 46 // LanguageOpts to describe a language to the code generator 47 type LanguageOpts struct { 48 ReservedWords []string 49 BaseImportFunc func(string) string `json:"-"` 50 reservedWordsSet map[string]struct{} 51 initialized bool 52 formatFunc func(string, []byte) ([]byte, error) 53 fileNameFunc func(string) string 54 } 55 56 // Init the language option 57 func (l *LanguageOpts) Init() { 58 if !l.initialized { 59 l.initialized = true 60 l.reservedWordsSet = make(map[string]struct{}) 61 for _, rw := range l.ReservedWords { 62 l.reservedWordsSet[rw] = struct{}{} 63 } 64 } 65 } 66 67 // MangleName makes sure a reserved word gets a safe name 68 func (l *LanguageOpts) MangleName(name, suffix string) string { 69 if _, ok := l.reservedWordsSet[swag.ToFileName(name)]; !ok { 70 return name 71 } 72 return strings.Join([]string{name, suffix}, "_") 73 } 74 75 // MangleVarName makes sure a reserved word gets a safe name 76 func (l *LanguageOpts) MangleVarName(name string) string { 77 nm := swag.ToVarName(name) 78 if _, ok := l.reservedWordsSet[nm]; !ok { 79 return nm 80 } 81 return nm + "Var" 82 } 83 84 // MangleFileName makes sure a file name gets a safe name 85 func (l *LanguageOpts) MangleFileName(name string) string { 86 if l.fileNameFunc != nil { 87 return l.fileNameFunc(name) 88 } 89 return swag.ToFileName(name) 90 } 91 92 // ManglePackageName makes sure a package gets a safe name. 93 // In case of a file system path (e.g. name contains "/" or "\" on Windows), this return only the last element. 94 func (l *LanguageOpts) ManglePackageName(name, suffix string) string { 95 if name == "" { 96 return suffix 97 } 98 pth := filepath.ToSlash(filepath.Clean(name)) // preserve path 99 _, pkg := path.Split(pth) // drop path 100 return l.MangleName(swag.ToFileName(pkg), suffix) 101 } 102 103 // ManglePackagePath makes sure a full package path gets a safe name. 104 // Only the last part of the path is altered. 105 func (l *LanguageOpts) ManglePackagePath(name string, suffix string) string { 106 if name == "" { 107 return suffix 108 } 109 target := filepath.ToSlash(filepath.Clean(name)) // preserve path 110 parts := strings.Split(target, "/") 111 parts[len(parts)-1] = l.ManglePackageName(parts[len(parts)-1], suffix) 112 return strings.Join(parts, "/") 113 } 114 115 // FormatContent formats a file with a language specific formatter 116 func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) { 117 if l.formatFunc != nil { 118 return l.formatFunc(name, content) 119 } 120 return content, nil 121 } 122 123 func (l *LanguageOpts) baseImport(tgt string) string { 124 if l.BaseImportFunc != nil { 125 return l.BaseImportFunc(tgt) 126 } 127 return "" 128 } 129 130 var golang = GoLangOpts() 131 132 // GoLangOpts for rendering items as golang code 133 func GoLangOpts() *LanguageOpts { 134 var goOtherReservedSuffixes = map[string]bool{ 135 // see: 136 // https://golang.org/src/go/build/syslist.go 137 // https://golang.org/doc/install/source#environment 138 139 // goos 140 "android": true, 141 "darwin": true, 142 "dragonfly": true, 143 "freebsd": true, 144 "js": true, 145 "linux": true, 146 "nacl": true, 147 "netbsd": true, 148 "openbsd": true, 149 "plan9": true, 150 "solaris": true, 151 "windows": true, 152 "zos": true, 153 154 // arch 155 "386": true, 156 "amd64": true, 157 "amd64p32": true, 158 "arm": true, 159 "armbe": true, 160 "arm64": true, 161 "arm64be": true, 162 "mips": true, 163 "mipsle": true, 164 "mips64": true, 165 "mips64le": true, 166 "mips64p32": true, 167 "mips64p32le": true, 168 "ppc": true, 169 "ppc64": true, 170 "ppc64le": true, 171 "riscv": true, 172 "riscv64": true, 173 "s390": true, 174 "s390x": true, 175 "sparc": true, 176 "sparc64": true, 177 "wasm": true, 178 179 // other reserved suffixes 180 "test": true, 181 } 182 183 opts := new(LanguageOpts) 184 opts.ReservedWords = []string{ 185 "break", "default", "func", "interface", "select", 186 "case", "defer", "go", "map", "struct", 187 "chan", "else", "goto", "package", "switch", 188 "const", "fallthrough", "if", "range", "type", 189 "continue", "for", "import", "return", "var", 190 } 191 opts.formatFunc = func(ffn string, content []byte) ([]byte, error) { 192 opts := new(imports.Options) 193 opts.TabIndent = true 194 opts.TabWidth = 2 195 opts.Fragment = true 196 opts.Comments = true 197 return imports.Process(ffn, content, opts) 198 } 199 opts.fileNameFunc = func(name string) string { 200 // whenever a generated file name ends with a suffix 201 // that is meaningful to go build, adds a "swagger" 202 // suffix 203 parts := strings.Split(swag.ToFileName(name), "_") 204 if goOtherReservedSuffixes[parts[len(parts)-1]] { 205 // file name ending with a reserved arch or os name 206 // are appended an innocuous suffix "swagger" 207 parts = append(parts, "swagger") 208 } 209 return strings.Join(parts, "_") 210 } 211 212 opts.BaseImportFunc = func(tgt string) string { 213 tgt = filepath.Clean(tgt) 214 // On Windows, filepath.Abs("") behaves differently than on Unix. 215 // Windows: yields an error, since Abs() does not know the volume. 216 // UNIX: returns current working directory 217 if tgt == "" { 218 tgt = "." 219 } 220 tgtAbsPath, err := filepath.Abs(tgt) 221 if err != nil { 222 log.Fatalf("could not evaluate base import path with target \"%s\": %v", tgt, err) 223 } 224 225 var tgtAbsPathExtended string 226 tgtAbsPathExtended, err = filepath.EvalSymlinks(tgtAbsPath) 227 if err != nil { 228 log.Fatalf("could not evaluate base import path with target \"%s\" (with symlink resolution): %v", tgtAbsPath, err) 229 } 230 231 gopath := os.Getenv("GOPATH") 232 if gopath == "" { 233 gopath = filepath.Join(os.Getenv("HOME"), "go") 234 } 235 236 var pth string 237 for _, gp := range filepath.SplitList(gopath) { 238 // EvalSymLinks also calls the Clean 239 gopathExtended, er := filepath.EvalSymlinks(gp) 240 if er != nil { 241 log.Fatalln(er) 242 } 243 gopathExtended = filepath.Join(gopathExtended, "src") 244 gp = filepath.Join(gp, "src") 245 246 // At this stage we have expanded and unexpanded target path. GOPATH is fully expanded. 247 // Expanded means symlink free. 248 // We compare both types of targetpath<s> with gopath. 249 // If any one of them coincides with gopath , it is imperative that 250 // target path lies inside gopath. How? 251 // - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths. 252 // - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path) 253 // - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path) 254 255 // Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if 256 // there are no symlinks. 257 258 // Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path) 259 // First if will fail. Second if will succeed. 260 261 // Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path) 262 // First if will succeed and break. 263 264 //compares non expanded path for both 265 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok { 266 pth = relativepath 267 break 268 } 269 270 // Compares non-expanded target path 271 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok { 272 pth = relativepath 273 break 274 } 275 276 // Compares expanded target path. 277 if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok { 278 pth = relativepath 279 break 280 } 281 282 } 283 284 mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath) 285 switch { 286 case err != nil: 287 log.Fatalf("Failed to resolve module using go.mod file: %s", err) 288 case mod != "": 289 relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath) 290 if !strings.HasSuffix(mod, relTgt) { 291 return mod + relTgt 292 } 293 return mod 294 } 295 296 if pth == "" { 297 log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module") 298 } 299 return pth 300 } 301 opts.Init() 302 return opts 303 } 304 305 var moduleRe = regexp.MustCompile(`module[ \t]+([^\s]+)`) 306 307 // resolveGoModFile walks up the directory tree starting from 'dir' until it 308 // finds a go.mod file. If go.mod is found it will return the related file 309 // object. If no go.mod file is found it will return an error. 310 func resolveGoModFile(dir string) (*os.File, string, error) { 311 goModPath := filepath.Join(dir, "go.mod") 312 f, err := os.Open(goModPath) 313 if err != nil { 314 if os.IsNotExist(err) && dir != filepath.Dir(dir) { 315 return resolveGoModFile(filepath.Dir(dir)) 316 } 317 return nil, "", err 318 } 319 return f, dir, nil 320 } 321 322 // relPathToRelGoPath takes a relative os path and returns the relative go 323 // package path. For unix nothing will change but for windows \ will be 324 // converted to /. 325 func relPathToRelGoPath(modAbsPath, absPath string) string { 326 if absPath == "." { 327 return "" 328 } 329 330 path := strings.TrimPrefix(absPath, modAbsPath) 331 pathItems := strings.Split(path, string(filepath.Separator)) 332 return strings.Join(pathItems, "/") 333 } 334 335 func tryResolveModule(baseTargetPath string) (string, string, error) { 336 f, goModAbsPath, err := resolveGoModFile(baseTargetPath) 337 switch { 338 case os.IsNotExist(err): 339 return "", "", nil 340 case err != nil: 341 return "", "", err 342 } 343 344 src, err := ioutil.ReadAll(f) 345 if err != nil { 346 return "", "", err 347 } 348 349 match := moduleRe.FindSubmatch(src) 350 if len(match) != 2 { 351 return "", "", nil 352 } 353 354 return string(match[1]), goModAbsPath, nil 355 } 356 357 func findSwaggerSpec(nm string) (string, error) { 358 specs := []string{"swagger.json", "swagger.yml", "swagger.yaml"} 359 if nm != "" { 360 specs = []string{nm} 361 } 362 var name string 363 for _, nn := range specs { 364 f, err := os.Stat(nn) 365 if err != nil && !os.IsNotExist(err) { 366 return "", err 367 } 368 if err != nil && os.IsNotExist(err) { 369 continue 370 } 371 if f.IsDir() { 372 return "", fmt.Errorf("%s is a directory", nn) 373 } 374 name = nn 375 break 376 } 377 if name == "" { 378 return "", errors.New("couldn't find a swagger spec") 379 } 380 return name, nil 381 } 382 383 // DefaultSectionOpts for a given opts, this is used when no config file is passed 384 // and uses the embedded templates when no local override can be found 385 func DefaultSectionOpts(gen *GenOpts) { 386 sec := gen.Sections 387 if len(sec.Models) == 0 { 388 sec.Models = []TemplateOpts{ 389 { 390 Name: "definition", 391 Source: "asset:model", 392 Target: "{{ joinFilePath .Target (toPackagePath .ModelPackage) }}", 393 FileName: "{{ (snakize (pascalize .Name)) }}.go", 394 }, 395 } 396 } 397 398 if len(sec.Operations) == 0 { 399 if gen.IsClient { 400 sec.Operations = []TemplateOpts{ 401 { 402 Name: "parameters", 403 Source: "asset:clientParameter", 404 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Package) }}", 405 FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", 406 }, 407 { 408 Name: "responses", 409 Source: "asset:clientResponse", 410 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Package) }}", 411 FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", 412 }, 413 } 414 415 } else { 416 ops := []TemplateOpts{} 417 if gen.IncludeParameters { 418 ops = append(ops, TemplateOpts{ 419 Name: "parameters", 420 Source: "asset:serverParameter", 421 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 422 FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", 423 }) 424 } 425 if gen.IncludeURLBuilder { 426 ops = append(ops, TemplateOpts{ 427 Name: "urlbuilder", 428 Source: "asset:serverUrlbuilder", 429 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 430 FileName: "{{ (snakize (pascalize .Name)) }}_urlbuilder.go", 431 }) 432 } 433 if gen.IncludeResponses { 434 ops = append(ops, TemplateOpts{ 435 Name: "responses", 436 Source: "asset:serverResponses", 437 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 438 FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", 439 }) 440 } 441 if gen.IncludeHandler { 442 ops = append(ops, TemplateOpts{ 443 Name: "handler", 444 Source: "asset:serverOperation", 445 Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 446 FileName: "{{ (snakize (pascalize .Name)) }}.go", 447 }) 448 } 449 sec.Operations = ops 450 } 451 } 452 453 if len(sec.OperationGroups) == 0 { 454 if gen.IsClient { 455 sec.OperationGroups = []TemplateOpts{ 456 { 457 Name: "client", 458 Source: "asset:clientClient", 459 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Name)}}", 460 FileName: "{{ (snakize (pascalize .Name)) }}_client.go", 461 }, 462 } 463 } else { 464 sec.OperationGroups = []TemplateOpts{} 465 } 466 } 467 468 if len(sec.Application) == 0 { 469 if gen.IsClient { 470 sec.Application = []TemplateOpts{ 471 { 472 Name: "facade", 473 Source: "asset:clientFacade", 474 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) }}", 475 FileName: "{{ snakize .Name }}Client.go", 476 }, 477 } 478 } else { 479 sec.Application = []TemplateOpts{ 480 { 481 Name: "configure", 482 Source: "asset:serverConfigureapi", 483 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 484 FileName: "configure_{{ (snakize (pascalize .Name)) }}.go", 485 SkipExists: !gen.RegenerateConfigureAPI, 486 }, 487 { 488 Name: "main", 489 Source: "asset:serverMain", 490 Target: "{{ joinFilePath .Target \"cmd\" (dasherize (pascalize .Name)) }}-server", 491 FileName: "main.go", 492 }, 493 { 494 Name: "embedded_spec", 495 Source: "asset:swaggerJsonEmbed", 496 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 497 FileName: "embedded_spec.go", 498 }, 499 { 500 Name: "server", 501 Source: "asset:serverServer", 502 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 503 FileName: "server.go", 504 }, 505 { 506 Name: "builder", 507 Source: "asset:serverBuilder", 508 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) }}", 509 FileName: "{{ snakize (pascalize .Name) }}_api.go", 510 }, 511 { 512 Name: "doc", 513 Source: "asset:serverDoc", 514 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 515 FileName: "doc.go", 516 }, 517 } 518 } 519 } 520 gen.Sections = sec 521 522 } 523 524 // TemplateOpts allows 525 type TemplateOpts struct { 526 Name string `mapstructure:"name"` 527 Source string `mapstructure:"source"` 528 Target string `mapstructure:"target"` 529 FileName string `mapstructure:"file_name"` 530 SkipExists bool `mapstructure:"skip_exists"` 531 SkipFormat bool `mapstructure:"skip_format"` 532 } 533 534 // SectionOpts allows for specifying options to customize the templates used for generation 535 type SectionOpts struct { 536 Application []TemplateOpts `mapstructure:"application"` 537 Operations []TemplateOpts `mapstructure:"operations"` 538 OperationGroups []TemplateOpts `mapstructure:"operation_groups"` 539 Models []TemplateOpts `mapstructure:"models"` 540 } 541 542 // GenOpts the options for the generator 543 type GenOpts struct { 544 IncludeModel bool 545 IncludeValidator bool 546 IncludeHandler bool 547 IncludeParameters bool 548 IncludeResponses bool 549 IncludeURLBuilder bool 550 IncludeMain bool 551 IncludeSupport bool 552 ExcludeSpec bool 553 DumpData bool 554 ValidateSpec bool 555 FlattenOpts *analysis.FlattenOpts 556 IsClient bool 557 defaultsEnsured bool 558 PropertiesSpecOrder bool 559 StrictAdditionalProperties bool 560 AllowTemplateOverride bool 561 562 Spec string 563 APIPackage string 564 ModelPackage string 565 ServerPackage string 566 ClientPackage string 567 Principal string 568 Target string 569 Sections SectionOpts 570 LanguageOpts *LanguageOpts 571 TypeMapping map[string]string 572 Imports map[string]string 573 DefaultScheme string 574 DefaultProduces string 575 DefaultConsumes string 576 TemplateDir string 577 Template string 578 RegenerateConfigureAPI bool 579 Operations []string 580 Models []string 581 Tags []string 582 Name string 583 FlagStrategy string 584 CompatibilityMode string 585 ExistingModels string 586 Copyright string 587 } 588 589 // CheckOpts carries out some global consistency checks on options. 590 // 591 // At the moment, these checks simply protect TargetPath() and SpecPath() 592 // functions. More checks may be added here. 593 func (g *GenOpts) CheckOpts() error { 594 if !filepath.IsAbs(g.Target) { 595 if _, err := filepath.Abs(g.Target); err != nil { 596 return fmt.Errorf("could not locate target %s: %v", g.Target, err) 597 } 598 } 599 if filepath.IsAbs(g.ServerPackage) { 600 return fmt.Errorf("you shouldn't specify an absolute path in --server-package: %s", g.ServerPackage) 601 } 602 if !filepath.IsAbs(g.Spec) && !strings.HasPrefix(g.Spec, "http://") && !strings.HasPrefix(g.Spec, "https://") { 603 if _, err := filepath.Abs(g.Spec); err != nil { 604 return fmt.Errorf("could not locate spec: %s", g.Spec) 605 } 606 } 607 return nil 608 } 609 610 // TargetPath returns the target generation path relative to the server package. 611 // This method is used by templates, e.g. with {{ .TargetPath }} 612 // 613 // Errors cases are prevented by calling CheckOpts beforehand. 614 // 615 // Example: 616 // Target: ${PWD}/tmp 617 // ServerPackage: abc/efg 618 // 619 // Server is generated in ${PWD}/tmp/abc/efg 620 // relative TargetPath returned: ../../../tmp 621 // 622 func (g *GenOpts) TargetPath() string { 623 var tgt string 624 if g.Target == "" { 625 tgt = "." // That's for windows 626 } else { 627 tgt = g.Target 628 } 629 tgtAbs, _ := filepath.Abs(tgt) 630 srvPkg := filepath.FromSlash(g.LanguageOpts.ManglePackagePath(g.ServerPackage, "server")) 631 srvrAbs := filepath.Join(tgtAbs, srvPkg) 632 tgtRel, _ := filepath.Rel(srvrAbs, filepath.Dir(tgtAbs)) 633 tgtRel = filepath.Join(tgtRel, filepath.Base(tgtAbs)) 634 return tgtRel 635 } 636 637 // SpecPath returns the path to the spec relative to the server package. 638 // If the spec is remote keep this absolute location. 639 // 640 // If spec is not relative to server (e.g. lives on a different drive on windows), 641 // then the resolved path is absolute. 642 // 643 // This method is used by templates, e.g. with {{ .SpecPath }} 644 // 645 // Errors cases are prevented by calling CheckOpts beforehand. 646 func (g *GenOpts) SpecPath() string { 647 if strings.HasPrefix(g.Spec, "http://") || strings.HasPrefix(g.Spec, "https://") { 648 return g.Spec 649 } 650 // Local specifications 651 specAbs, _ := filepath.Abs(g.Spec) 652 var tgt string 653 if g.Target == "" { 654 tgt = "." // That's for windows 655 } else { 656 tgt = g.Target 657 } 658 tgtAbs, _ := filepath.Abs(tgt) 659 srvPkg := filepath.FromSlash(g.LanguageOpts.ManglePackagePath(g.ServerPackage, "server")) 660 srvAbs := filepath.Join(tgtAbs, srvPkg) 661 specRel, err := filepath.Rel(srvAbs, specAbs) 662 if err != nil { 663 return specAbs 664 } 665 return specRel 666 } 667 668 // EnsureDefaults for these gen opts 669 func (g *GenOpts) EnsureDefaults() error { 670 if g.defaultsEnsured { 671 return nil 672 } 673 DefaultSectionOpts(g) 674 if g.LanguageOpts == nil { 675 g.LanguageOpts = GoLangOpts() 676 } 677 // set defaults for flattening options 678 g.FlattenOpts = &analysis.FlattenOpts{ 679 Minimal: true, 680 Verbose: true, 681 RemoveUnused: false, 682 Expand: false, 683 } 684 g.defaultsEnsured = true 685 return nil 686 } 687 688 func (g *GenOpts) location(t *TemplateOpts, data interface{}) (string, string, error) { 689 v := reflect.Indirect(reflect.ValueOf(data)) 690 fld := v.FieldByName("Name") 691 var name string 692 if fld.IsValid() { 693 log.Println("name field", fld.String()) 694 name = fld.String() 695 } 696 697 fldpack := v.FieldByName("Package") 698 pkg := g.APIPackage 699 if fldpack.IsValid() { 700 log.Println("package field", fldpack.String()) 701 pkg = fldpack.String() 702 } 703 704 var tags []string 705 tagsF := v.FieldByName("Tags") 706 if tagsF.IsValid() { 707 tags = tagsF.Interface().([]string) 708 } 709 710 pthTpl, err := template.New(t.Name + "-target").Funcs(FuncMap).Parse(t.Target) 711 if err != nil { 712 return "", "", err 713 } 714 715 fNameTpl, err := template.New(t.Name + "-filename").Funcs(FuncMap).Parse(t.FileName) 716 if err != nil { 717 return "", "", err 718 } 719 720 d := struct { 721 Name, Package, APIPackage, ServerPackage, ClientPackage, ModelPackage, Target string 722 Tags []string 723 }{ 724 Name: name, 725 Package: pkg, 726 APIPackage: g.APIPackage, 727 ServerPackage: g.ServerPackage, 728 ClientPackage: g.ClientPackage, 729 ModelPackage: g.ModelPackage, 730 Target: g.Target, 731 Tags: tags, 732 } 733 734 // pretty.Println(data) 735 var pthBuf bytes.Buffer 736 if e := pthTpl.Execute(&pthBuf, d); e != nil { 737 return "", "", e 738 } 739 740 var fNameBuf bytes.Buffer 741 if e := fNameTpl.Execute(&fNameBuf, d); e != nil { 742 return "", "", e 743 } 744 return pthBuf.String(), fileName(fNameBuf.String()), nil 745 } 746 747 func (g *GenOpts) render(t *TemplateOpts, data interface{}) ([]byte, error) { 748 var templ *template.Template 749 750 if strings.HasPrefix(strings.ToLower(t.Source), "asset:") { 751 tt, err := templates.Get(strings.TrimPrefix(t.Source, "asset:")) 752 if err != nil { 753 return nil, err 754 } 755 templ = tt 756 } 757 758 if templ == nil { 759 // try to load from repository (and enable dependencies) 760 name := swag.ToJSONName(strings.TrimSuffix(t.Source, ".gotmpl")) 761 tt, err := templates.Get(name) 762 if err == nil { 763 templ = tt 764 } 765 } 766 767 if templ == nil { 768 // try to load template from disk, in TemplateDir if specified 769 // (dependencies resolution is limited to preloaded assets) 770 var templateFile string 771 if g.TemplateDir != "" { 772 templateFile = filepath.Join(g.TemplateDir, t.Source) 773 } else { 774 templateFile = t.Source 775 } 776 content, err := ioutil.ReadFile(templateFile) 777 if err != nil { 778 return nil, fmt.Errorf("error while opening %s template file: %v", templateFile, err) 779 } 780 tt, err := template.New(t.Source).Funcs(FuncMap).Parse(string(content)) 781 if err != nil { 782 return nil, fmt.Errorf("template parsing failed on template %s: %v", t.Name, err) 783 } 784 templ = tt 785 } 786 787 if templ == nil { 788 return nil, fmt.Errorf("template %q not found", t.Source) 789 } 790 791 var tBuf bytes.Buffer 792 if err := templ.Execute(&tBuf, data); err != nil { 793 return nil, fmt.Errorf("template execution failed for template %s: %v", t.Name, err) 794 } 795 log.Printf("executed template %s", t.Source) 796 797 return tBuf.Bytes(), nil 798 } 799 800 // Render template and write generated source code 801 // generated code is reformatted ("linted"), which gives an 802 // additional level of checking. If this step fails, the generated 803 // code is still dumped, for template debugging purposes. 804 func (g *GenOpts) write(t *TemplateOpts, data interface{}) error { 805 dir, fname, err := g.location(t, data) 806 if err != nil { 807 return fmt.Errorf("failed to resolve template location for template %s: %v", t.Name, err) 808 } 809 810 if t.SkipExists && fileExists(dir, fname) { 811 debugLog("skipping generation of %s because it already exists and skip_exist directive is set for %s", 812 filepath.Join(dir, fname), t.Name) 813 return nil 814 } 815 816 log.Printf("creating generated file %q in %q as %s", fname, dir, t.Name) 817 content, err := g.render(t, data) 818 if err != nil { 819 return fmt.Errorf("failed rendering template data for %s: %v", t.Name, err) 820 } 821 822 if dir != "" { 823 _, exists := os.Stat(dir) 824 if os.IsNotExist(exists) { 825 debugLog("creating directory %q for \"%s\"", dir, t.Name) 826 // Directory settings consistent with file privileges. 827 // Environment's umask may alter this setup 828 if e := os.MkdirAll(dir, 0755); e != nil { 829 return e 830 } 831 } 832 } 833 834 // Conditionally format the code, unless the user wants to skip 835 formatted := content 836 var writeerr error 837 838 if !t.SkipFormat { 839 formatted, err = g.LanguageOpts.FormatContent(fname, content) 840 if err != nil { 841 log.Printf("source formatting failed on template-generated source (%q for %s). Check that your template produces valid code", filepath.Join(dir, fname), t.Name) 842 writeerr = ioutil.WriteFile(filepath.Join(dir, fname), content, 0644) 843 if writeerr != nil { 844 return fmt.Errorf("failed to write (unformatted) file %q in %q: %v", fname, dir, writeerr) 845 } 846 log.Printf("unformatted generated source %q has been dumped for template debugging purposes. DO NOT build on this source!", fname) 847 return fmt.Errorf("source formatting on generated source %q failed: %v", t.Name, err) 848 } 849 } 850 851 writeerr = ioutil.WriteFile(filepath.Join(dir, fname), formatted, 0644) 852 if writeerr != nil { 853 return fmt.Errorf("failed to write file %q in %q: %v", fname, dir, writeerr) 854 } 855 return err 856 } 857 858 func fileName(in string) string { 859 ext := filepath.Ext(in) 860 return swag.ToFileName(strings.TrimSuffix(in, ext)) + ext 861 } 862 863 func (g *GenOpts) shouldRenderApp(t *TemplateOpts, app *GenApp) bool { 864 switch swag.ToFileName(swag.ToGoName(t.Name)) { 865 case "main": 866 return g.IncludeMain 867 case "embedded_spec": 868 return !g.ExcludeSpec 869 default: 870 return true 871 } 872 } 873 874 func (g *GenOpts) shouldRenderOperations() bool { 875 return g.IncludeHandler || g.IncludeParameters || g.IncludeResponses 876 } 877 878 func (g *GenOpts) renderApplication(app *GenApp) error { 879 log.Printf("rendering %d templates for application %s", len(g.Sections.Application), app.Name) 880 for _, templ := range g.Sections.Application { 881 if !g.shouldRenderApp(&templ, app) { 882 continue 883 } 884 if err := g.write(&templ, app); err != nil { 885 return err 886 } 887 } 888 return nil 889 } 890 891 func (g *GenOpts) renderOperationGroup(gg *GenOperationGroup) error { 892 log.Printf("rendering %d templates for operation group %s", len(g.Sections.OperationGroups), g.Name) 893 for _, templ := range g.Sections.OperationGroups { 894 if !g.shouldRenderOperations() { 895 continue 896 } 897 898 if err := g.write(&templ, gg); err != nil { 899 return err 900 } 901 } 902 return nil 903 } 904 905 func (g *GenOpts) renderOperation(gg *GenOperation) error { 906 log.Printf("rendering %d templates for operation %s", len(g.Sections.Operations), g.Name) 907 for _, templ := range g.Sections.Operations { 908 if !g.shouldRenderOperations() { 909 continue 910 } 911 912 if err := g.write(&templ, gg); err != nil { 913 return err 914 } 915 } 916 return nil 917 } 918 919 func (g *GenOpts) renderDefinition(gg *GenDefinition) error { 920 log.Printf("rendering %d templates for model %s", len(g.Sections.Models), gg.Name) 921 for _, templ := range g.Sections.Models { 922 if !g.IncludeModel { 923 continue 924 } 925 926 if err := g.write(&templ, gg); err != nil { 927 return err 928 } 929 } 930 return nil 931 } 932 933 func validateSpec(path string, doc *loads.Document) (err error) { 934 if doc == nil { 935 if path, doc, err = loadSpec(path); err != nil { 936 return err 937 } 938 } 939 940 result := validate.Spec(doc, strfmt.Default) 941 if result == nil { 942 return nil 943 } 944 945 str := fmt.Sprintf("The swagger spec at %q is invalid against swagger specification %s. see errors :\n", path, doc.Version()) 946 for _, desc := range result.(*swaggererrors.CompositeError).Errors { 947 str += fmt.Sprintf("- %s\n", desc) 948 } 949 return errors.New(str) 950 } 951 952 func loadSpec(specFile string) (string, *loads.Document, error) { 953 // find swagger spec document, verify it exists 954 specPath := specFile 955 var err error 956 if !strings.HasPrefix(specPath, "http") { 957 specPath, err = findSwaggerSpec(specFile) 958 if err != nil { 959 return "", nil, err 960 } 961 } 962 963 // load swagger spec 964 specDoc, err := loads.Spec(specPath) 965 if err != nil { 966 return "", nil, err 967 } 968 return specPath, specDoc, nil 969 } 970 971 func fileExists(target, name string) bool { 972 _, err := os.Stat(filepath.Join(target, name)) 973 return !os.IsNotExist(err) 974 } 975 976 func gatherModels(specDoc *loads.Document, modelNames []string) (map[string]spec.Schema, error) { 977 models, mnc := make(map[string]spec.Schema), len(modelNames) 978 defs := specDoc.Spec().Definitions 979 980 if mnc > 0 { 981 var unknownModels []string 982 for _, m := range modelNames { 983 _, ok := defs[m] 984 if !ok { 985 unknownModels = append(unknownModels, m) 986 } 987 } 988 if len(unknownModels) != 0 { 989 return nil, fmt.Errorf("unknown models: %s", strings.Join(unknownModels, ", ")) 990 } 991 } 992 for k, v := range defs { 993 if mnc == 0 { 994 models[k] = v 995 } 996 for _, nm := range modelNames { 997 if k == nm { 998 models[k] = v 999 } 1000 } 1001 } 1002 return models, nil 1003 } 1004 1005 func appNameOrDefault(specDoc *loads.Document, name, defaultName string) string { 1006 if strings.TrimSpace(name) == "" { 1007 if specDoc.Spec().Info != nil && strings.TrimSpace(specDoc.Spec().Info.Title) != "" { 1008 name = specDoc.Spec().Info.Title 1009 } else { 1010 name = defaultName 1011 } 1012 } 1013 return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(swag.ToGoName(name), "Test"), "API"), "Test") 1014 } 1015 1016 func containsString(names []string, name string) bool { 1017 for _, nm := range names { 1018 if nm == name { 1019 return true 1020 } 1021 } 1022 return false 1023 } 1024 1025 type opRef struct { 1026 Method string 1027 Path string 1028 Key string 1029 ID string 1030 Op *spec.Operation 1031 } 1032 1033 type opRefs []opRef 1034 1035 func (o opRefs) Len() int { return len(o) } 1036 func (o opRefs) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 1037 func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key } 1038 1039 func gatherOperations(specDoc *analysis.Spec, operationIDs []string) map[string]opRef { 1040 var oprefs opRefs 1041 1042 for method, pathItem := range specDoc.Operations() { 1043 for path, operation := range pathItem { 1044 // nm := ensureUniqueName(operation.ID, method, path, operations) 1045 vv := *operation 1046 oprefs = append(oprefs, opRef{ 1047 Key: swag.ToGoName(strings.ToLower(method) + " " + path), 1048 Method: method, 1049 Path: path, 1050 ID: vv.ID, 1051 Op: &vv, 1052 }) 1053 } 1054 } 1055 1056 sort.Sort(oprefs) 1057 1058 operations := make(map[string]opRef) 1059 for _, opr := range oprefs { 1060 nm := opr.ID 1061 if nm == "" { 1062 nm = opr.Key 1063 } 1064 1065 oo, found := operations[nm] 1066 if found && oo.Method != opr.Method && oo.Path != opr.Path { 1067 nm = opr.Key 1068 } 1069 if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) { 1070 opr.ID = nm 1071 opr.Op.ID = nm 1072 operations[nm] = opr 1073 } 1074 } 1075 1076 return operations 1077 } 1078 1079 func pascalize(arg string) string { 1080 runes := []rune(arg) 1081 switch len(runes) { 1082 case 0: 1083 return "" 1084 case 1: // handle special case when we have a single rune that is not handled by swag.ToGoName 1085 switch runes[0] { 1086 case '+', '-', '#', '_': // those cases are handled differently than swag utility 1087 return prefixForName(arg) 1088 } 1089 } 1090 return swag.ToGoName(swag.ToGoName(arg)) // want to remove spaces 1091 } 1092 1093 func prefixForName(arg string) string { 1094 first := []rune(arg)[0] 1095 if len(arg) == 0 || unicode.IsLetter(first) { 1096 return "" 1097 } 1098 switch first { 1099 case '+': 1100 return "Plus" 1101 case '-': 1102 return "Minus" 1103 case '#': 1104 return "HashTag" 1105 // other cases ($,@ etc..) handled by swag.ToGoName 1106 } 1107 return "Nr" 1108 } 1109 1110 func init() { 1111 // this makes the ToGoName func behave with the special 1112 // prefixing rule above 1113 swag.GoNamePrefixFunc = prefixForName 1114 } 1115 1116 func pruneEmpty(in []string) (out []string) { 1117 for _, v := range in { 1118 if v != "" { 1119 out = append(out, v) 1120 } 1121 } 1122 return 1123 } 1124 1125 func trimBOM(in string) string { 1126 return strings.Trim(in, "\xef\xbb\xbf") 1127 } 1128 1129 func validateAndFlattenSpec(opts *GenOpts, specDoc *loads.Document) (*loads.Document, error) { 1130 1131 var err error 1132 1133 // Validate if needed 1134 if opts.ValidateSpec { 1135 log.Printf("validating spec %v", opts.Spec) 1136 if erv := validateSpec(opts.Spec, specDoc); erv != nil { 1137 return specDoc, erv 1138 } 1139 } 1140 1141 // Restore spec to original 1142 opts.Spec, specDoc, err = loadSpec(opts.Spec) 1143 if err != nil { 1144 return nil, err 1145 } 1146 1147 absBasePath := specDoc.SpecFilePath() 1148 if !filepath.IsAbs(absBasePath) { 1149 cwd, _ := os.Getwd() 1150 absBasePath = filepath.Join(cwd, absBasePath) 1151 } 1152 1153 // Some preprocessing is required before codegen 1154 // 1155 // This ensures at least that $ref's in the spec document are canonical, 1156 // i.e all $ref are local to this file and point to some uniquely named definition. 1157 // 1158 // Default option is to ensure minimal flattening of $ref, bundling remote $refs and relocating arbitrary JSON 1159 // pointers as definitions. 1160 // This preprocessing may introduce duplicate names (e.g. remote $ref with same name). In this case, a definition 1161 // suffixed with "OAIGen" is produced. 1162 // 1163 // Full flattening option farther transforms the spec by moving every complex object (e.g. with some properties) 1164 // as a standalone definition. 1165 // 1166 // Eventually, an "expand spec" option is available. It is essentially useful for testing purposes. 1167 // 1168 // NOTE(fredbi): spec expansion may produce some unsupported constructs and is not yet protected against the 1169 // following cases: 1170 // - polymorphic types generation may fail with expansion (expand destructs the reuse intent of the $ref in allOf) 1171 // - name duplicates may occur and result in compilation failures 1172 // The right place to fix these shortcomings is go-openapi/analysis. 1173 1174 opts.FlattenOpts.BasePath = absBasePath // BasePath must be absolute 1175 opts.FlattenOpts.Spec = analysis.New(specDoc.Spec()) 1176 1177 var preprocessingOption string 1178 switch { 1179 case opts.FlattenOpts.Expand: 1180 preprocessingOption = "expand" 1181 case opts.FlattenOpts.Minimal: 1182 preprocessingOption = "minimal flattening" 1183 default: 1184 preprocessingOption = "full flattening" 1185 } 1186 log.Printf("preprocessing spec with option: %s", preprocessingOption) 1187 1188 if err = analysis.Flatten(*opts.FlattenOpts); err != nil { 1189 return nil, err 1190 } 1191 1192 // yields the preprocessed spec document 1193 return specDoc, nil 1194 } 1195 1196 // gatherSecuritySchemes produces a sorted representation from a map of spec security schemes 1197 func gatherSecuritySchemes(securitySchemes map[string]spec.SecurityScheme, appName, principal, receiver string) (security GenSecuritySchemes) { 1198 for scheme, req := range securitySchemes { 1199 isOAuth2 := strings.ToLower(req.Type) == "oauth2" 1200 var scopes []string 1201 if isOAuth2 { 1202 for k := range req.Scopes { 1203 scopes = append(scopes, k) 1204 } 1205 } 1206 sort.Strings(scopes) 1207 1208 security = append(security, GenSecurityScheme{ 1209 AppName: appName, 1210 ID: scheme, 1211 ReceiverName: receiver, 1212 Name: req.Name, 1213 IsBasicAuth: strings.ToLower(req.Type) == "basic", 1214 IsAPIKeyAuth: strings.ToLower(req.Type) == "apikey", 1215 IsOAuth2: isOAuth2, 1216 Scopes: scopes, 1217 Principal: principal, 1218 Source: req.In, 1219 // from original spec 1220 Description: req.Description, 1221 Type: strings.ToLower(req.Type), 1222 In: req.In, 1223 Flow: req.Flow, 1224 AuthorizationURL: req.AuthorizationURL, 1225 TokenURL: req.TokenURL, 1226 Extensions: req.Extensions, 1227 }) 1228 } 1229 sort.Sort(security) 1230 return 1231 } 1232 1233 // gatherExtraSchemas produces a sorted list of extra schemas. 1234 // 1235 // ExtraSchemas are inlined types rendered in the same model file. 1236 func gatherExtraSchemas(extraMap map[string]GenSchema) (extras GenSchemaList) { 1237 var extraKeys []string 1238 for k := range extraMap { 1239 extraKeys = append(extraKeys, k) 1240 } 1241 sort.Strings(extraKeys) 1242 for _, k := range extraKeys { 1243 // figure out if top level validations are needed 1244 p := extraMap[k] 1245 p.HasValidations = shallowValidationLookup(p) 1246 extras = append(extras, p) 1247 } 1248 return 1249 } 1250 1251 func sharedValidationsFromSimple(v spec.CommonValidations, isRequired bool) (sh sharedValidations) { 1252 sh = sharedValidations{ 1253 Required: isRequired, 1254 Maximum: v.Maximum, 1255 ExclusiveMaximum: v.ExclusiveMaximum, 1256 Minimum: v.Minimum, 1257 ExclusiveMinimum: v.ExclusiveMinimum, 1258 MaxLength: v.MaxLength, 1259 MinLength: v.MinLength, 1260 Pattern: v.Pattern, 1261 MaxItems: v.MaxItems, 1262 MinItems: v.MinItems, 1263 UniqueItems: v.UniqueItems, 1264 MultipleOf: v.MultipleOf, 1265 Enum: v.Enum, 1266 } 1267 return 1268 } 1269 1270 func sharedValidationsFromSchema(v spec.Schema, isRequired bool) (sh sharedValidations) { 1271 sh = sharedValidations{ 1272 Required: isRequired, 1273 Maximum: v.Maximum, 1274 ExclusiveMaximum: v.ExclusiveMaximum, 1275 Minimum: v.Minimum, 1276 ExclusiveMinimum: v.ExclusiveMinimum, 1277 MaxLength: v.MaxLength, 1278 MinLength: v.MinLength, 1279 Pattern: v.Pattern, 1280 MaxItems: v.MaxItems, 1281 MinItems: v.MinItems, 1282 UniqueItems: v.UniqueItems, 1283 MultipleOf: v.MultipleOf, 1284 Enum: v.Enum, 1285 } 1286 return 1287 }