github.com/go-swagger/go-swagger@v0.31.0/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 "encoding/json" 20 "errors" 21 "fmt" 22 "log" 23 "os" 24 "path" 25 "path/filepath" 26 "reflect" 27 "sort" 28 "strings" 29 "text/template" 30 31 "github.com/go-openapi/analysis" 32 "github.com/go-openapi/loads" 33 "github.com/go-openapi/runtime" 34 "github.com/go-openapi/spec" 35 "github.com/go-openapi/swag" 36 ) 37 38 const ( 39 // default generation targets structure 40 defaultModelsTarget = "models" 41 defaultServerTarget = "restapi" 42 defaultClientTarget = "client" 43 defaultOperationsTarget = "operations" 44 defaultClientName = "rest" 45 defaultServerName = "swagger" 46 defaultScheme = "http" 47 defaultImplementationTarget = "implementation" 48 ) 49 50 func init() { 51 // all initializations for the generator package 52 debugOptions() 53 initLanguage() 54 initTemplateRepo() 55 initTypes() 56 } 57 58 // DefaultSectionOpts for a given opts, this is used when no config file is passed 59 // and uses the embedded templates when no local override can be found 60 func DefaultSectionOpts(gen *GenOpts) { 61 sec := gen.Sections 62 if len(sec.Models) == 0 { 63 opts := []TemplateOpts{ 64 { 65 Name: "definition", 66 Source: "asset:model", 67 Target: "{{ joinFilePath .Target (toPackagePath .ModelPackage) }}", 68 FileName: "{{ (snakize (pascalize .Name)) }}.go", 69 }, 70 } 71 sec.Models = opts 72 } 73 74 if len(sec.PostModels) == 0 && gen.IncludeCLi { 75 // For CLI, we need to postpone the generation of model-supporting source, 76 // in order for go imports to run properly in all cases. 77 opts := []TemplateOpts{ 78 { 79 Name: "clidefinitionhook", 80 Source: "asset:cliModelcli", 81 Target: "{{ joinFilePath .Target (toPackagePath .CliPackage) }}", 82 FileName: "{{ (snakize (pascalize .Name)) }}_model.go", 83 }, 84 } 85 sec.PostModels = opts 86 } 87 88 if len(sec.Operations) == 0 { 89 if gen.IsClient { 90 opts := []TemplateOpts{ 91 { 92 Name: "parameters", 93 Source: "asset:clientParameter", 94 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Package) }}", 95 FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", 96 }, 97 { 98 Name: "responses", 99 Source: "asset:clientResponse", 100 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Package) }}", 101 FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", 102 }, 103 } 104 if gen.IncludeCLi { 105 opts = append(opts, TemplateOpts{ 106 Name: "clioperation", 107 Source: "asset:cliOperation", 108 Target: "{{ joinFilePath .Target (toPackagePath .CliPackage) }}", 109 FileName: "{{ (snakize (pascalize .Name)) }}_operation.go", 110 }) 111 } 112 sec.Operations = opts 113 } else { 114 ops := []TemplateOpts{} 115 if gen.IncludeParameters { 116 ops = append(ops, TemplateOpts{ 117 Name: "parameters", 118 Source: "asset:serverParameter", 119 Target: "{{ if .UseTags }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 120 FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", 121 }) 122 } 123 if gen.IncludeURLBuilder { 124 ops = append(ops, TemplateOpts{ 125 Name: "urlbuilder", 126 Source: "asset:serverUrlbuilder", 127 Target: "{{ if .UseTags }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 128 FileName: "{{ (snakize (pascalize .Name)) }}_urlbuilder.go", 129 }) 130 } 131 if gen.IncludeResponses { 132 ops = append(ops, TemplateOpts{ 133 Name: "responses", 134 Source: "asset:serverResponses", 135 Target: "{{ if .UseTags }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 136 FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", 137 }) 138 } 139 if gen.IncludeHandler { 140 ops = append(ops, TemplateOpts{ 141 Name: "handler", 142 Source: "asset:serverOperation", 143 Target: "{{ if .UseTags }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", 144 FileName: "{{ (snakize (pascalize .Name)) }}.go", 145 }) 146 } 147 sec.Operations = ops 148 } 149 } 150 151 if len(sec.OperationGroups) == 0 { 152 if gen.IsClient { 153 sec.OperationGroups = []TemplateOpts{ 154 { 155 Name: "client", 156 Source: "asset:clientClient", 157 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Name)}}", 158 FileName: "{{ (snakize (pascalize .Name)) }}_client.go", 159 }, 160 } 161 } else { 162 sec.OperationGroups = []TemplateOpts{} 163 } 164 } 165 166 if len(sec.Application) == 0 { 167 if gen.IsClient { 168 opts := []TemplateOpts{ 169 { 170 Name: "facade", 171 Source: "asset:clientFacade", 172 Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) }}", 173 FileName: "{{ snakize .Name }}Client.go", 174 }, 175 } 176 if gen.IncludeCLi { 177 // include a commandline tool app 178 opts = append(opts, []TemplateOpts{{ 179 Name: "commandline", 180 Source: "asset:cliCli", 181 Target: "{{ joinFilePath .Target (toPackagePath .CliPackage) }}", 182 FileName: "cli.go", 183 }, { 184 Name: "climain", 185 Source: "asset:cliMain", 186 Target: "{{ joinFilePath .Target \"cmd\" (toPackagePath .CliAppName) }}", 187 FileName: "main.go", 188 }, { 189 Name: "cliAutoComplete", 190 Source: "asset:cliCompletion", 191 Target: "{{ joinFilePath .Target (toPackagePath .CliPackage) }}", 192 FileName: "autocomplete.go", 193 }}...) 194 } 195 sec.Application = opts 196 } else { 197 opts := []TemplateOpts{ 198 { 199 Name: "main", 200 Source: "asset:serverMain", 201 Target: "{{ joinFilePath .Target \"cmd\" .MainPackage }}", 202 FileName: "main.go", 203 }, 204 { 205 Name: "embedded_spec", 206 Source: "asset:swaggerJsonEmbed", 207 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 208 FileName: "embedded_spec.go", 209 }, 210 { 211 Name: "server", 212 Source: "asset:serverServer", 213 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 214 FileName: "server.go", 215 }, 216 { 217 Name: "builder", 218 Source: "asset:serverBuilder", 219 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) }}", 220 FileName: "{{ snakize (pascalize .Name) }}_api.go", 221 }, 222 { 223 Name: "doc", 224 Source: "asset:serverDoc", 225 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 226 FileName: "doc.go", 227 }, 228 } 229 if gen.ImplementationPackage != "" { 230 // Use auto configure template 231 opts = append(opts, TemplateOpts{ 232 Name: "autoconfigure", 233 Source: "asset:serverAutoconfigureapi", 234 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 235 FileName: "auto_configure_{{ (snakize (pascalize .Name)) }}.go", 236 }) 237 } else { 238 opts = append(opts, TemplateOpts{ 239 Name: "configure", 240 Source: "asset:serverConfigureapi", 241 Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", 242 FileName: "configure_{{ (snakize (pascalize .Name)) }}.go", 243 SkipExists: !gen.RegenerateConfigureAPI, 244 }) 245 } 246 sec.Application = opts 247 } 248 } 249 gen.Sections = sec 250 } 251 252 // MarkdownOpts for rendering a spec as markdown 253 func MarkdownOpts() *LanguageOpts { 254 opts := &LanguageOpts{} 255 opts.Init() 256 return opts 257 } 258 259 // MarkdownSectionOpts for a given opts and output file. 260 func MarkdownSectionOpts(gen *GenOpts, output string) { 261 gen.Sections.Models = nil 262 gen.Sections.PostModels = nil 263 gen.Sections.OperationGroups = nil 264 gen.Sections.Operations = nil 265 gen.LanguageOpts = MarkdownOpts() 266 gen.Sections.Application = []TemplateOpts{ 267 { 268 Name: "markdowndocs", 269 Source: "asset:markdownDocs", 270 Target: filepath.Dir(output), 271 FileName: filepath.Base(output), 272 }, 273 } 274 } 275 276 // TemplateOpts allows for codegen customization 277 type TemplateOpts struct { 278 Name string `mapstructure:"name"` 279 Source string `mapstructure:"source"` 280 Target string `mapstructure:"target"` 281 FileName string `mapstructure:"file_name"` 282 SkipExists bool `mapstructure:"skip_exists"` 283 SkipFormat bool `mapstructure:"skip_format"` 284 } 285 286 // SectionOpts allows for specifying options to customize the templates used for generation 287 type SectionOpts struct { 288 Application []TemplateOpts `mapstructure:"application"` 289 Operations []TemplateOpts `mapstructure:"operations"` 290 OperationGroups []TemplateOpts `mapstructure:"operation_groups"` 291 Models []TemplateOpts `mapstructure:"models"` 292 PostModels []TemplateOpts `mapstructure:"post_models"` 293 } 294 295 // GenOptsCommon the options for the generator 296 type GenOptsCommon struct { 297 IncludeModel bool 298 IncludeValidator bool 299 IncludeHandler bool 300 IncludeParameters bool 301 IncludeResponses bool 302 IncludeURLBuilder bool 303 IncludeMain bool 304 IncludeSupport bool 305 IncludeCLi bool 306 ExcludeSpec bool 307 DumpData bool 308 ValidateSpec bool 309 FlattenOpts *analysis.FlattenOpts 310 IsClient bool 311 defaultsEnsured bool 312 PropertiesSpecOrder bool 313 StrictAdditionalProperties bool 314 AllowTemplateOverride bool 315 316 Spec string 317 APIPackage string 318 ModelPackage string 319 ServerPackage string 320 ClientPackage string 321 CliPackage string 322 CliAppName string // name of cli app. For example "dockerctl" 323 ImplementationPackage string 324 Principal string 325 PrincipalCustomIface bool // user-provided interface for Principal (non-nullable) 326 Target string // dir location where generated code is written to 327 Sections SectionOpts 328 LanguageOpts *LanguageOpts 329 TypeMapping map[string]string 330 Imports map[string]string 331 DefaultScheme string 332 DefaultProduces string 333 DefaultConsumes string 334 WithXML bool 335 TemplateDir string 336 Template string 337 RegenerateConfigureAPI bool 338 Operations []string 339 Models []string 340 Tags []string 341 StructTags []string 342 Name string 343 FlagStrategy string 344 CompatibilityMode string 345 ExistingModels string 346 Copyright string 347 SkipTagPackages bool 348 MainPackage string 349 IgnoreOperations bool 350 AllowEnumCI bool 351 StrictResponders bool 352 AcceptDefinitionsOnly bool 353 WantsRootedErrorPath bool 354 355 templates *Repository // a shallow clone of the global template repository 356 } 357 358 // CheckOpts carries out some global consistency checks on options. 359 func (g *GenOpts) CheckOpts() error { 360 if g == nil { 361 return errors.New("gen opts are required") 362 } 363 364 if !filepath.IsAbs(g.Target) { 365 if _, err := filepath.Abs(g.Target); err != nil { 366 return fmt.Errorf("could not locate target %s: %w", g.Target, err) 367 } 368 } 369 370 if filepath.IsAbs(g.ServerPackage) { 371 return fmt.Errorf("you shouldn't specify an absolute path in --server-package: %s", g.ServerPackage) 372 } 373 374 if strings.HasPrefix(g.Spec, "http://") || strings.HasPrefix(g.Spec, "https://") { 375 return nil 376 } 377 378 pth, err := findSwaggerSpec(g.Spec) 379 if err != nil { 380 return err 381 } 382 383 // ensure spec path is absolute 384 g.Spec, err = filepath.Abs(pth) 385 if err != nil { 386 return fmt.Errorf("could not locate spec: %s", g.Spec) 387 } 388 389 return nil 390 } 391 392 // TargetPath returns the target generation path relative to the server package. 393 // This method is used by templates, e.g. with {{ .TargetPath }} 394 // 395 // Errors cases are prevented by calling CheckOpts beforehand. 396 // 397 // Example: 398 // Target: ${PWD}/tmp 399 // ServerPackage: abc/efg 400 // 401 // Server is generated in ${PWD}/tmp/abc/efg 402 // relative TargetPath returned: ../../../tmp 403 func (g *GenOpts) TargetPath() string { 404 var tgt string 405 if g.Target == "" { 406 tgt = "." // That's for windows 407 } else { 408 tgt = g.Target 409 } 410 tgtAbs, _ := filepath.Abs(tgt) 411 srvPkg := filepath.FromSlash(g.LanguageOpts.ManglePackagePath(g.ServerPackage, "server")) 412 srvrAbs := filepath.Join(tgtAbs, srvPkg) 413 tgtRel, _ := filepath.Rel(srvrAbs, filepath.Dir(tgtAbs)) 414 tgtRel = filepath.Join(tgtRel, filepath.Base(tgtAbs)) 415 return tgtRel 416 } 417 418 // SpecPath returns the path to the spec relative to the server package. 419 // If the spec is remote keep this absolute location. 420 // 421 // If spec is not relative to server (e.g. lives on a different drive on windows), 422 // then the resolved path is absolute. 423 // 424 // This method is used by templates, e.g. with {{ .SpecPath }} 425 // 426 // Errors cases are prevented by calling CheckOpts beforehand. 427 func (g *GenOpts) SpecPath() string { 428 if strings.HasPrefix(g.Spec, "http://") || strings.HasPrefix(g.Spec, "https://") { 429 return g.Spec 430 } 431 // Local specifications 432 specAbs, _ := filepath.Abs(g.Spec) 433 var tgt string 434 if g.Target == "" { 435 tgt = "." // That's for windows 436 } else { 437 tgt = g.Target 438 } 439 tgtAbs, _ := filepath.Abs(tgt) 440 srvPkg := filepath.FromSlash(g.LanguageOpts.ManglePackagePath(g.ServerPackage, "server")) 441 srvAbs := filepath.Join(tgtAbs, srvPkg) 442 specRel, err := filepath.Rel(srvAbs, specAbs) 443 if err != nil { 444 return specAbs 445 } 446 return specRel 447 } 448 449 // PrincipalIsNullable indicates whether the principal type used for authentication 450 // may be used as a pointer 451 func (g *GenOpts) PrincipalIsNullable() bool { 452 debugLog("Principal: %s, %t, isnullable: %t", g.Principal, g.PrincipalCustomIface, g.Principal != iface && !g.PrincipalCustomIface) 453 return g.Principal != iface && !g.PrincipalCustomIface 454 } 455 456 // EnsureDefaults for these gen opts 457 func (g *GenOpts) EnsureDefaults() error { 458 if g.defaultsEnsured { 459 return nil 460 } 461 462 g.templates = templates.ShallowClone() 463 464 g.templates.LoadDefaults() 465 466 if g.LanguageOpts == nil { 467 g.LanguageOpts = DefaultLanguageFunc() 468 } 469 470 DefaultSectionOpts(g) 471 472 // set defaults for flattening options 473 if g.FlattenOpts == nil { 474 g.FlattenOpts = &analysis.FlattenOpts{ 475 Minimal: true, 476 Verbose: true, 477 RemoveUnused: false, 478 Expand: false, 479 } 480 } 481 482 if g.DefaultScheme == "" { 483 g.DefaultScheme = defaultScheme 484 } 485 486 if g.DefaultConsumes == "" { 487 g.DefaultConsumes = runtime.JSONMime 488 } 489 490 if g.DefaultProduces == "" { 491 g.DefaultProduces = runtime.JSONMime 492 } 493 494 // always include validator with models 495 g.IncludeValidator = true 496 497 if g.Principal == "" { 498 g.Principal = iface 499 g.PrincipalCustomIface = false 500 } 501 502 g.defaultsEnsured = true 503 return nil 504 } 505 506 func (g *GenOpts) location(t *TemplateOpts, data interface{}) (string, string, error) { 507 v := reflect.Indirect(reflect.ValueOf(data)) 508 fld := v.FieldByName("Name") 509 var name string 510 if fld.IsValid() { 511 log.Println("name field", fld.String()) 512 name = fld.String() 513 } 514 515 fldpack := v.FieldByName("Package") 516 pkg := g.APIPackage 517 if fldpack.IsValid() { 518 log.Println("package field", fldpack.String()) 519 pkg = fldpack.String() 520 } 521 522 var tags []string 523 tagsF := v.FieldByName("Tags") 524 if tagsF.IsValid() { 525 if tt, ok := tagsF.Interface().([]string); ok { 526 tags = tt 527 } 528 } 529 530 var useTags bool 531 useTagsF := v.FieldByName("UseTags") 532 if useTagsF.IsValid() { 533 useTags = useTagsF.Interface().(bool) 534 } 535 536 funcMap := FuncMapFunc(g.LanguageOpts) 537 538 pthTpl, err := template.New(t.Name + "-target").Funcs(funcMap).Parse(t.Target) 539 if err != nil { 540 return "", "", err 541 } 542 543 fNameTpl, err := template.New(t.Name + "-filename").Funcs(funcMap).Parse(t.FileName) 544 if err != nil { 545 return "", "", err 546 } 547 548 d := struct { 549 Name, CliAppName, Package, APIPackage, ServerPackage, ClientPackage, CliPackage, ModelPackage, MainPackage, Target string 550 Tags []string 551 UseTags bool 552 Context interface{} 553 }{ 554 Name: name, 555 CliAppName: g.CliAppName, 556 Package: pkg, 557 APIPackage: g.APIPackage, 558 ServerPackage: g.ServerPackage, 559 ClientPackage: g.ClientPackage, 560 CliPackage: g.CliPackage, 561 ModelPackage: g.ModelPackage, 562 MainPackage: g.MainPackage, 563 Target: g.Target, 564 Tags: tags, 565 UseTags: useTags, 566 Context: data, 567 } 568 569 var pthBuf bytes.Buffer 570 if e := pthTpl.Execute(&pthBuf, d); e != nil { 571 return "", "", e 572 } 573 574 var fNameBuf bytes.Buffer 575 if e := fNameTpl.Execute(&fNameBuf, d); e != nil { 576 return "", "", e 577 } 578 return pthBuf.String(), fileName(fNameBuf.String()), nil 579 } 580 581 func (g *GenOpts) render(t *TemplateOpts, data interface{}) ([]byte, error) { 582 var templ *template.Template 583 584 if strings.HasPrefix(strings.ToLower(t.Source), "asset:") { 585 tt, err := g.templates.Get(strings.TrimPrefix(t.Source, "asset:")) 586 if err != nil { 587 return nil, err 588 } 589 templ = tt 590 } 591 592 if templ == nil { 593 // try to load from repository (and enable dependencies) 594 name := swag.ToJSONName(strings.TrimSuffix(t.Source, ".gotmpl")) 595 tt, err := g.templates.Get(name) 596 if err == nil { 597 templ = tt 598 } 599 } 600 601 if templ == nil { 602 // try to load template from disk, in TemplateDir if specified 603 // (dependencies resolution is limited to preloaded assets) 604 var templateFile string 605 if g.TemplateDir != "" { 606 templateFile = filepath.Join(g.TemplateDir, t.Source) 607 } else { 608 templateFile = t.Source 609 } 610 content, err := os.ReadFile(templateFile) 611 if err != nil { 612 return nil, fmt.Errorf("error while opening %s template file: %w", templateFile, err) 613 } 614 tt, err := template.New(t.Source).Funcs(FuncMapFunc(g.LanguageOpts)).Parse(string(content)) 615 if err != nil { 616 return nil, fmt.Errorf("template parsing failed on template %s: %w", t.Name, err) 617 } 618 templ = tt 619 } 620 621 if templ == nil { 622 return nil, fmt.Errorf("template %q not found", t.Source) 623 } 624 625 var tBuf bytes.Buffer 626 if err := templ.Execute(&tBuf, data); err != nil { 627 return nil, fmt.Errorf("template execution failed for template %s: %w", t.Name, err) 628 } 629 log.Printf("executed template %s", t.Source) 630 631 return tBuf.Bytes(), nil 632 } 633 634 // Render template and write generated source code 635 // generated code is reformatted ("linted"), which gives an 636 // additional level of checking. If this step fails, the generated 637 // code is still dumped, for template debugging purposes. 638 func (g *GenOpts) write(t *TemplateOpts, data interface{}) error { 639 dir, fname, err := g.location(t, data) 640 if err != nil { 641 return fmt.Errorf("failed to resolve template location for template %s: %w", t.Name, err) 642 } 643 644 if t.SkipExists && fileExists(dir, fname) { 645 debugLog("skipping generation of %s because it already exists and skip_exist directive is set for %s", 646 filepath.Join(dir, fname), t.Name) 647 return nil 648 } 649 650 log.Printf("creating generated file %q in %q as %s", fname, dir, t.Name) 651 content, err := g.render(t, data) 652 if err != nil { 653 return fmt.Errorf("failed rendering template data for %s: %w", t.Name, err) 654 } 655 656 if dir != "" { 657 _, exists := os.Stat(dir) 658 if os.IsNotExist(exists) { 659 debugLog("creating directory %q for \"%s\"", dir, t.Name) 660 // Directory settings consistent with file privileges. 661 // Environment's umask may alter this setup 662 if e := os.MkdirAll(dir, 0o755); e != nil { 663 return e 664 } 665 } 666 } 667 668 // Conditionally format the code, unless the user wants to skip 669 formatted := content 670 var writeerr error 671 672 if !t.SkipFormat { 673 formatted, err = g.LanguageOpts.FormatContent(filepath.Join(dir, fname), content) 674 if err != nil { 675 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) 676 writeerr = os.WriteFile(filepath.Join(dir, fname), content, 0o644) // #nosec 677 if writeerr != nil { 678 return fmt.Errorf("failed to write (unformatted) file %q in %q: %w", fname, dir, writeerr) 679 } 680 log.Printf("unformatted generated source %q has been dumped for template debugging purposes. DO NOT build on this source!", fname) 681 return fmt.Errorf("source formatting on generated source %q failed: %w", t.Name, err) 682 } 683 } 684 685 writeerr = os.WriteFile(filepath.Join(dir, fname), formatted, 0o644) // #nosec 686 if writeerr != nil { 687 return fmt.Errorf("failed to write file %q in %q: %w", fname, dir, writeerr) 688 } 689 return err 690 } 691 692 func fileName(in string) string { 693 ext := filepath.Ext(in) 694 return swag.ToFileName(strings.TrimSuffix(in, ext)) + ext 695 } 696 697 func (g *GenOpts) shouldRenderApp(t *TemplateOpts, _ *GenApp) bool { 698 switch swag.ToFileName(swag.ToGoName(t.Name)) { 699 case "main": 700 return g.IncludeMain 701 case "embedded_spec": 702 return !g.ExcludeSpec 703 default: 704 return true 705 } 706 } 707 708 func (g *GenOpts) shouldRenderOperations() bool { 709 return g.IncludeHandler || g.IncludeParameters || g.IncludeResponses 710 } 711 712 func (g *GenOpts) renderApplication(app *GenApp) error { 713 log.Printf("rendering %d templates for application %s", len(g.Sections.Application), app.Name) 714 for _, tp := range g.Sections.Application { 715 templ := tp 716 if !g.shouldRenderApp(&templ, app) { 717 continue 718 } 719 if err := g.write(&templ, app); err != nil { 720 return err 721 } 722 } 723 724 if len(g.Sections.PostModels) > 0 { 725 log.Printf("post-rendering from %d models", len(app.Models)) 726 for _, templateToPin := range g.Sections.PostModels { 727 templateConfig := templateToPin 728 for _, modelToPin := range app.Models { 729 modelData := modelToPin 730 if err := g.write(&templateConfig, modelData); err != nil { 731 return err 732 } 733 } 734 } 735 } 736 737 return nil 738 } 739 740 func (g *GenOpts) renderOperationGroup(gg *GenOperationGroup) error { 741 log.Printf("rendering %d templates for operation group %s", len(g.Sections.OperationGroups), g.Name) 742 for _, tp := range g.Sections.OperationGroups { 743 templ := tp 744 if !g.shouldRenderOperations() { 745 continue 746 } 747 748 if err := g.write(&templ, gg); err != nil { 749 return err 750 } 751 } 752 return nil 753 } 754 755 func (g *GenOpts) renderOperation(gg *GenOperation) error { 756 log.Printf("rendering %d templates for operation %s", len(g.Sections.Operations), g.Name) 757 for _, tp := range g.Sections.Operations { 758 templ := tp 759 if !g.shouldRenderOperations() { 760 continue 761 } 762 763 if err := g.write(&templ, gg); err != nil { 764 return err 765 } 766 } 767 return nil 768 } 769 770 func (g *GenOpts) renderDefinition(gg *GenDefinition) error { 771 log.Printf("rendering %d templates for model %s", len(g.Sections.Models), gg.Name) 772 for _, tp := range g.Sections.Models { 773 templ := tp 774 if !g.IncludeModel { 775 continue 776 } 777 778 if err := g.write(&templ, gg); err != nil { 779 return err 780 } 781 } 782 return nil 783 } 784 785 func (g *GenOptsCommon) setTemplates() error { 786 if g.Template != "" { 787 // set contrib templates 788 if err := g.templates.LoadContrib(g.Template); err != nil { 789 return err 790 } 791 } 792 793 g.templates.SetAllowOverride(g.AllowTemplateOverride) 794 795 if g.TemplateDir != "" { 796 // set custom templates 797 if err := g.templates.LoadDir(g.TemplateDir); err != nil { 798 return err 799 } 800 } 801 return nil 802 } 803 804 // defaultImports produces a default map for imports with models 805 func (g *GenOpts) defaultImports() map[string]string { 806 baseImport := g.LanguageOpts.baseImport(g.Target) 807 defaultImports := make(map[string]string, 50) 808 809 var modelsAlias, importPath string 810 if g.ExistingModels == "" { 811 // generated models 812 importPath = path.Join( 813 baseImport, 814 g.LanguageOpts.ManglePackagePath(g.ModelPackage, defaultModelsTarget)) 815 modelsAlias = g.LanguageOpts.ManglePackageName(g.ModelPackage, defaultModelsTarget) 816 } else { 817 // external models 818 importPath = g.LanguageOpts.ManglePackagePath(g.ExistingModels, "") 819 modelsAlias = path.Base(defaultModelsTarget) 820 } 821 defaultImports[modelsAlias] = importPath 822 823 // resolve model representing an authenticated principal 824 alias, _, target := g.resolvePrincipal() 825 if alias == "" || target == g.ModelPackage || path.Base(target) == modelsAlias { 826 // if principal is specified with the models generation package, do not import any extra package 827 return defaultImports 828 } 829 830 if pth, _ := path.Split(target); pth != "" { 831 // if principal is specified with a path, assume this is a fully qualified package and generate this import 832 defaultImports[alias] = target 833 } else { 834 // if principal is specified with a relative path (no "/", e.g. internal.Principal), assume it is located in generated target 835 defaultImports[alias] = path.Join(baseImport, target) 836 } 837 return defaultImports 838 } 839 840 // initImports produces a default map for import with the specified root for operations 841 func (g *GenOpts) initImports(operationsPackage string) map[string]string { 842 baseImport := g.LanguageOpts.baseImport(g.Target) 843 844 imports := make(map[string]string, 50) 845 imports[g.LanguageOpts.ManglePackageName(operationsPackage, defaultOperationsTarget)] = path.Join( 846 baseImport, 847 g.LanguageOpts.ManglePackagePath(operationsPackage, defaultOperationsTarget)) 848 return imports 849 } 850 851 // PrincipalAlias returns an aliased type to the principal 852 func (g *GenOpts) PrincipalAlias() string { 853 _, principal, _ := g.resolvePrincipal() 854 return principal 855 } 856 857 func (g *GenOpts) resolvePrincipal() (string, string, string) { 858 dotLocation := strings.LastIndex(g.Principal, ".") 859 if dotLocation < 0 { 860 return "", g.Principal, "" 861 } 862 863 // handle possible conflicts with injected principal package 864 // NOTE(fred): we do not check here for conflicts with packages created from operation tags, only standard imports 865 alias := deconflictPrincipal(importAlias(g.Principal[:dotLocation])) 866 return alias, alias + g.Principal[dotLocation:], g.Principal[:dotLocation] 867 } 868 869 func fileExists(target, name string) bool { 870 _, err := os.Stat(filepath.Join(target, name)) 871 return !os.IsNotExist(err) 872 } 873 874 func gatherModels(specDoc *loads.Document, modelNames []string) (map[string]spec.Schema, error) { 875 modelNames = pruneEmpty(modelNames) 876 models, mnc := make(map[string]spec.Schema), len(modelNames) 877 defs := specDoc.Spec().Definitions 878 879 if mnc > 0 { 880 var unknownModels []string 881 for _, m := range modelNames { 882 _, ok := defs[m] 883 if !ok { 884 unknownModels = append(unknownModels, m) 885 } 886 } 887 if len(unknownModels) != 0 { 888 return nil, fmt.Errorf("unknown models: %s", strings.Join(unknownModels, ", ")) 889 } 890 } 891 for k, v := range defs { 892 if mnc == 0 { 893 models[k] = v 894 } 895 for _, nm := range modelNames { 896 if k == nm { 897 models[k] = v 898 } 899 } 900 } 901 return models, nil 902 } 903 904 // titleOrDefault infers a name for the app from the title of the spec 905 func titleOrDefault(specDoc *loads.Document, name, defaultName string) string { 906 if strings.TrimSpace(name) == "" { 907 if specDoc.Spec().Info != nil && strings.TrimSpace(specDoc.Spec().Info.Title) != "" { 908 name = specDoc.Spec().Info.Title 909 } else { 910 name = defaultName 911 } 912 } 913 return swag.ToGoName(name) 914 } 915 916 func mainNameOrDefault(specDoc *loads.Document, name, defaultName string) string { 917 // *_test won't do as main server name 918 return strings.TrimSuffix(titleOrDefault(specDoc, name, defaultName), "Test") 919 } 920 921 func appNameOrDefault(specDoc *loads.Document, name, defaultName string) string { 922 // *_test won't do as app names 923 name = strings.TrimSuffix(titleOrDefault(specDoc, name, defaultName), "Test") 924 if name == "" { 925 name = swag.ToGoName(defaultName) 926 } 927 return name 928 } 929 930 type opRef struct { 931 Method string 932 Path string 933 Key string 934 ID string 935 Op *spec.Operation 936 } 937 938 type opRefs []opRef 939 940 func (o opRefs) Len() int { return len(o) } 941 func (o opRefs) Swap(i, j int) { o[i], o[j] = o[j], o[i] } 942 func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key } 943 944 func gatherOperations(specDoc *analysis.Spec, operationIDs []string) map[string]opRef { 945 operationIDs = pruneEmpty(operationIDs) 946 var oprefs opRefs 947 948 for method, pathItem := range specDoc.Operations() { 949 for path, operation := range pathItem { 950 vv := *operation 951 oprefs = append(oprefs, opRef{ 952 Key: swag.ToGoName(strings.ToLower(method) + " " + swag.ToHumanNameTitle(path)), 953 Method: method, 954 Path: path, 955 ID: vv.ID, 956 Op: &vv, 957 }) 958 } 959 } 960 961 sort.Sort(oprefs) 962 963 operations := make(map[string]opRef) 964 for _, opr := range oprefs { 965 nm := opr.ID 966 if nm == "" { 967 nm = opr.Key 968 } 969 970 oo, found := operations[nm] 971 if found && oo.Method != opr.Method && oo.Path != opr.Path { 972 nm = opr.Key 973 } 974 if len(operationIDs) == 0 || swag.ContainsStrings(operationIDs, opr.ID) || swag.ContainsStrings(operationIDs, nm) { 975 opr.ID = nm 976 opr.Op.ID = nm 977 operations[nm] = opr 978 } 979 } 980 981 return operations 982 } 983 984 func pruneEmpty(in []string) (out []string) { 985 for _, v := range in { 986 if v != "" { 987 out = append(out, v) 988 } 989 } 990 return 991 } 992 993 func trimBOM(in string) string { 994 return strings.Trim(in, "\xef\xbb\xbf") 995 } 996 997 // gatherSecuritySchemes produces a sorted representation from a map of spec security schemes 998 func gatherSecuritySchemes(securitySchemes map[string]spec.SecurityScheme, appName, principal, receiver string, nullable bool) (security GenSecuritySchemes) { 999 for scheme, req := range securitySchemes { 1000 isOAuth2 := strings.ToLower(req.Type) == "oauth2" 1001 scopes := make([]string, 0, len(req.Scopes)) 1002 genScopes := make([]GenSecurityScope, 0, len(req.Scopes)) 1003 if isOAuth2 { 1004 for k, v := range req.Scopes { 1005 scopes = append(scopes, k) 1006 genScopes = append(genScopes, GenSecurityScope{Name: k, Description: v}) 1007 } 1008 sort.Strings(scopes) 1009 } 1010 1011 security = append(security, GenSecurityScheme{ 1012 AppName: appName, 1013 ID: scheme, 1014 ReceiverName: receiver, 1015 Name: req.Name, 1016 IsBasicAuth: strings.ToLower(req.Type) == "basic", 1017 IsAPIKeyAuth: strings.ToLower(req.Type) == "apikey", 1018 IsOAuth2: isOAuth2, 1019 Scopes: scopes, 1020 ScopesDesc: genScopes, 1021 Principal: principal, 1022 Source: req.In, 1023 // from original spec 1024 Description: req.Description, 1025 Type: strings.ToLower(req.Type), 1026 In: req.In, 1027 Flow: req.Flow, 1028 AuthorizationURL: req.AuthorizationURL, 1029 TokenURL: req.TokenURL, 1030 Extensions: req.Extensions, 1031 1032 PrincipalIsNullable: nullable, 1033 }) 1034 } 1035 sort.Sort(security) 1036 return 1037 } 1038 1039 // securityRequirements just clones the original SecurityRequirements from either the spec 1040 // or an operation, without any modification. This is used to generate documentation. 1041 func securityRequirements(orig []map[string][]string) (result []analysis.SecurityRequirement) { 1042 for _, r := range orig { 1043 for k, v := range r { 1044 result = append(result, analysis.SecurityRequirement{Name: k, Scopes: v}) 1045 } 1046 } 1047 // TODO(fred): sort this for stable generation 1048 return 1049 } 1050 1051 // gatherExtraSchemas produces a sorted list of extra schemas. 1052 // 1053 // ExtraSchemas are inlined types rendered in the same model file. 1054 func gatherExtraSchemas(extraMap map[string]GenSchema) (extras GenSchemaList) { 1055 var extraKeys []string 1056 for k := range extraMap { 1057 extraKeys = append(extraKeys, k) 1058 } 1059 sort.Strings(extraKeys) 1060 for _, k := range extraKeys { 1061 // figure out if top level validations are needed 1062 p := extraMap[k] 1063 p.HasValidations = shallowValidationLookup(p) 1064 extras = append(extras, p) 1065 } 1066 return 1067 } 1068 1069 func getExtraSchemes(ext spec.Extensions) []string { 1070 if ess, ok := ext.GetStringSlice(xSchemes); ok { 1071 return ess 1072 } 1073 return nil 1074 } 1075 1076 func gatherURISchemes(swsp *spec.Swagger, operation spec.Operation) ([]string, []string) { 1077 var extraSchemes []string 1078 extraSchemes = append(extraSchemes, getExtraSchemes(operation.Extensions)...) 1079 extraSchemes = concatUnique(getExtraSchemes(swsp.Extensions), extraSchemes) 1080 sort.Strings(extraSchemes) 1081 1082 schemes := concatUnique(swsp.Schemes, operation.Schemes) 1083 sort.Strings(schemes) 1084 1085 return schemes, extraSchemes 1086 } 1087 1088 func dumpData(data interface{}) error { 1089 bb, err := json.MarshalIndent(data, "", " ") 1090 if err != nil { 1091 return err 1092 } 1093 fmt.Fprintln(os.Stdout, string(bb)) // TODO(fred): not testable 1094 return nil 1095 } 1096 1097 func importAlias(pkg string) string { 1098 _, k := path.Split(pkg) 1099 return k 1100 } 1101 1102 // concatUnique concatenate collections of strings with deduplication 1103 func concatUnique(collections ...[]string) []string { 1104 resultSet := make(map[string]struct{}) 1105 for _, c := range collections { 1106 for _, i := range c { 1107 if _, ok := resultSet[i]; !ok { 1108 resultSet[i] = struct{}{} 1109 } 1110 } 1111 } 1112 result := make([]string, 0, len(resultSet)) 1113 for k := range resultSet { 1114 result = append(result, k) 1115 } 1116 return result 1117 }