// Copyright 2015 go-swagger maintainers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package generator import ( "bytes" "errors" "fmt" "io/ioutil" "log" "os" "path" "path/filepath" "reflect" "regexp" "sort" "strings" "text/template" "unicode" swaggererrors "github.com/go-openapi/errors" "github.com/go-openapi/analysis" "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/go-openapi/validate" "golang.org/x/tools/imports" ) //go:generate go-bindata -mode 420 -modtime 1482416923 -pkg=generator -ignore=.*\.sw? -ignore=.*\.md ./templates/... // LanguageOpts to describe a language to the code generator type LanguageOpts struct { ReservedWords []string BaseImportFunc func(string) string `json:"-"` reservedWordsSet map[string]struct{} initialized bool formatFunc func(string, []byte) ([]byte, error) fileNameFunc func(string) string } // Init the language option func (l *LanguageOpts) Init() { if !l.initialized { l.initialized = true l.reservedWordsSet = make(map[string]struct{}) for _, rw := range l.ReservedWords { l.reservedWordsSet[rw] = struct{}{} } } } // MangleName makes sure a reserved word gets a safe name func (l *LanguageOpts) MangleName(name, suffix string) string { if _, ok := l.reservedWordsSet[swag.ToFileName(name)]; !ok { return name } return strings.Join([]string{name, suffix}, "_") } // MangleVarName makes sure a reserved word gets a safe name func (l *LanguageOpts) MangleVarName(name string) string { nm := swag.ToVarName(name) if _, ok := l.reservedWordsSet[nm]; !ok { return nm } return nm + "Var" } // MangleFileName makes sure a file name gets a safe name func (l *LanguageOpts) MangleFileName(name string) string { if l.fileNameFunc != nil { return l.fileNameFunc(name) } return swag.ToFileName(name) } // ManglePackageName makes sure a package gets a safe name. // In case of a file system path (e.g. name contains "/" or "\" on Windows), this return only the last element. func (l *LanguageOpts) ManglePackageName(name, suffix string) string { if name == "" { return suffix } pth := filepath.ToSlash(filepath.Clean(name)) // preserve path _, pkg := path.Split(pth) // drop path return l.MangleName(swag.ToFileName(pkg), suffix) } // ManglePackagePath makes sure a full package path gets a safe name. // Only the last part of the path is altered. func (l *LanguageOpts) ManglePackagePath(name string, suffix string) string { if name == "" { return suffix } target := filepath.ToSlash(filepath.Clean(name)) // preserve path parts := strings.Split(target, "/") parts[len(parts)-1] = l.ManglePackageName(parts[len(parts)-1], suffix) return strings.Join(parts, "/") } // FormatContent formats a file with a language specific formatter func (l *LanguageOpts) FormatContent(name string, content []byte) ([]byte, error) { if l.formatFunc != nil { return l.formatFunc(name, content) } return content, nil } func (l *LanguageOpts) baseImport(tgt string) string { if l.BaseImportFunc != nil { return l.BaseImportFunc(tgt) } return "" } var golang = GoLangOpts() // GoLangOpts for rendering items as golang code func GoLangOpts() *LanguageOpts { var goOtherReservedSuffixes = map[string]bool{ // see: // https://golang.org/src/go/build/syslist.go // https://golang.org/doc/install/source#environment // goos "android": true, "darwin": true, "dragonfly": true, "freebsd": true, "js": true, "linux": true, "nacl": true, "netbsd": true, "openbsd": true, "plan9": true, "solaris": true, "windows": true, "zos": true, // arch "386": true, "amd64": true, "amd64p32": true, "arm": true, "armbe": true, "arm64": true, "arm64be": true, "mips": true, "mipsle": true, "mips64": true, "mips64le": true, "mips64p32": true, "mips64p32le": true, "ppc": true, "ppc64": true, "ppc64le": true, "riscv": true, "riscv64": true, "s390": true, "s390x": true, "sparc": true, "sparc64": true, "wasm": true, // other reserved suffixes "test": true, } opts := new(LanguageOpts) opts.ReservedWords = []string{ "break", "default", "func", "interface", "select", "case", "defer", "go", "map", "struct", "chan", "else", "goto", "package", "switch", "const", "fallthrough", "if", "range", "type", "continue", "for", "import", "return", "var", } opts.formatFunc = func(ffn string, content []byte) ([]byte, error) { opts := new(imports.Options) opts.TabIndent = true opts.TabWidth = 2 opts.Fragment = true opts.Comments = true return imports.Process(ffn, content, opts) } opts.fileNameFunc = func(name string) string { // whenever a generated file name ends with a suffix // that is meaningful to go build, adds a "swagger" // suffix parts := strings.Split(swag.ToFileName(name), "_") if goOtherReservedSuffixes[parts[len(parts)-1]] { // file name ending with a reserved arch or os name // are appended an innocuous suffix "swagger" parts = append(parts, "swagger") } return strings.Join(parts, "_") } opts.BaseImportFunc = func(tgt string) string { tgt = filepath.Clean(tgt) // On Windows, filepath.Abs("") behaves differently than on Unix. // Windows: yields an error, since Abs() does not know the volume. // UNIX: returns current working directory if tgt == "" { tgt = "." } tgtAbsPath, err := filepath.Abs(tgt) if err != nil { log.Fatalf("could not evaluate base import path with target \"%s\": %v", tgt, err) } var tgtAbsPathExtended string tgtAbsPathExtended, err = filepath.EvalSymlinks(tgtAbsPath) if err != nil { log.Fatalf("could not evaluate base import path with target \"%s\" (with symlink resolution): %v", tgtAbsPath, err) } gopath := os.Getenv("GOPATH") if gopath == "" { gopath = filepath.Join(os.Getenv("HOME"), "go") } var pth string for _, gp := range filepath.SplitList(gopath) { // EvalSymLinks also calls the Clean gopathExtended, er := filepath.EvalSymlinks(gp) if er != nil { log.Fatalln(er) } gopathExtended = filepath.Join(gopathExtended, "src") gp = filepath.Join(gp, "src") // At this stage we have expanded and unexpanded target path. GOPATH is fully expanded. // Expanded means symlink free. // We compare both types of targetpath with gopath. // If any one of them coincides with gopath , it is imperative that // target path lies inside gopath. How? // - Case 1: Irrespective of symlinks paths coincide. Both non-expanded paths. // - Case 2: Symlink in target path points to location inside GOPATH. (Expanded Target Path) // - Case 3: Symlink in target path points to directory outside GOPATH (Unexpanded target path) // Case 1: - Do nothing case. If non-expanded paths match just generate base import path as if // there are no symlinks. // Case 2: - Symlink in target path points to location inside GOPATH. (Expanded Target Path) // First if will fail. Second if will succeed. // Case 3: - Symlink in target path points to directory outside GOPATH (Unexpanded target path) // First if will succeed and break. //compares non expanded path for both if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gp); ok { pth = relativepath break } // Compares non-expanded target path if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPath, gopathExtended); ok { pth = relativepath break } // Compares expanded target path. if ok, relativepath := checkPrefixAndFetchRelativePath(tgtAbsPathExtended, gopathExtended); ok { pth = relativepath break } } mod, goModuleAbsPath, err := tryResolveModule(tgtAbsPath) switch { case err != nil: log.Fatalf("Failed to resolve module using go.mod file: %s", err) case mod != "": relTgt := relPathToRelGoPath(goModuleAbsPath, tgtAbsPath) if !strings.HasSuffix(mod, relTgt) { return mod + relTgt } return mod } if pth == "" { log.Fatalln("target must reside inside a location in the $GOPATH/src or be a module") } return pth } opts.Init() return opts } var moduleRe = regexp.MustCompile(`module[ \t]+([^\s]+)`) // resolveGoModFile walks up the directory tree starting from 'dir' until it // finds a go.mod file. If go.mod is found it will return the related file // object. If no go.mod file is found it will return an error. func resolveGoModFile(dir string) (*os.File, string, error) { goModPath := filepath.Join(dir, "go.mod") f, err := os.Open(goModPath) if err != nil { if os.IsNotExist(err) && dir != filepath.Dir(dir) { return resolveGoModFile(filepath.Dir(dir)) } return nil, "", err } return f, dir, nil } // relPathToRelGoPath takes a relative os path and returns the relative go // package path. For unix nothing will change but for windows \ will be // converted to /. func relPathToRelGoPath(modAbsPath, absPath string) string { if absPath == "." { return "" } path := strings.TrimPrefix(absPath, modAbsPath) pathItems := strings.Split(path, string(filepath.Separator)) return strings.Join(pathItems, "/") } func tryResolveModule(baseTargetPath string) (string, string, error) { f, goModAbsPath, err := resolveGoModFile(baseTargetPath) switch { case os.IsNotExist(err): return "", "", nil case err != nil: return "", "", err } src, err := ioutil.ReadAll(f) if err != nil { return "", "", err } match := moduleRe.FindSubmatch(src) if len(match) != 2 { return "", "", nil } return string(match[1]), goModAbsPath, nil } func findSwaggerSpec(nm string) (string, error) { specs := []string{"swagger.json", "swagger.yml", "swagger.yaml"} if nm != "" { specs = []string{nm} } var name string for _, nn := range specs { f, err := os.Stat(nn) if err != nil && !os.IsNotExist(err) { return "", err } if err != nil && os.IsNotExist(err) { continue } if f.IsDir() { return "", fmt.Errorf("%s is a directory", nn) } name = nn break } if name == "" { return "", errors.New("couldn't find a swagger spec") } return name, nil } // DefaultSectionOpts for a given opts, this is used when no config file is passed // and uses the embedded templates when no local override can be found func DefaultSectionOpts(gen *GenOpts) { sec := gen.Sections if len(sec.Models) == 0 { sec.Models = []TemplateOpts{ { Name: "definition", Source: "asset:model", Target: "{{ joinFilePath .Target (toPackagePath .ModelPackage) }}", FileName: "{{ (snakize (pascalize .Name)) }}.go", }, } } if len(sec.Operations) == 0 { if gen.IsClient { sec.Operations = []TemplateOpts{ { Name: "parameters", Source: "asset:clientParameter", Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Package) }}", FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", }, { Name: "responses", Source: "asset:clientResponse", Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Package) }}", FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", }, } } else { ops := []TemplateOpts{} if gen.IncludeParameters { ops = append(ops, TemplateOpts{ Name: "parameters", Source: "asset:serverParameter", Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", FileName: "{{ (snakize (pascalize .Name)) }}_parameters.go", }) } if gen.IncludeURLBuilder { ops = append(ops, TemplateOpts{ Name: "urlbuilder", Source: "asset:serverUrlbuilder", Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", FileName: "{{ (snakize (pascalize .Name)) }}_urlbuilder.go", }) } if gen.IncludeResponses { ops = append(ops, TemplateOpts{ Name: "responses", Source: "asset:serverResponses", Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", FileName: "{{ (snakize (pascalize .Name)) }}_responses.go", }) } if gen.IncludeHandler { ops = append(ops, TemplateOpts{ Name: "handler", Source: "asset:serverOperation", Target: "{{ if gt (len .Tags) 0 }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) (toPackagePath .Package) }}{{ else }}{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .Package) }}{{ end }}", FileName: "{{ (snakize (pascalize .Name)) }}.go", }) } sec.Operations = ops } } if len(sec.OperationGroups) == 0 { if gen.IsClient { sec.OperationGroups = []TemplateOpts{ { Name: "client", Source: "asset:clientClient", Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) (toPackagePath .Name)}}", FileName: "{{ (snakize (pascalize .Name)) }}_client.go", }, } } else { sec.OperationGroups = []TemplateOpts{} } } if len(sec.Application) == 0 { if gen.IsClient { sec.Application = []TemplateOpts{ { Name: "facade", Source: "asset:clientFacade", Target: "{{ joinFilePath .Target (toPackagePath .ClientPackage) }}", FileName: "{{ snakize .Name }}Client.go", }, } } else { sec.Application = []TemplateOpts{ { Name: "configure", Source: "asset:serverConfigureapi", Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", FileName: "configure_{{ (snakize (pascalize .Name)) }}.go", SkipExists: !gen.RegenerateConfigureAPI, }, { Name: "main", Source: "asset:serverMain", Target: "{{ joinFilePath .Target \"cmd\" (dasherize (pascalize .Name)) }}-server", FileName: "main.go", }, { Name: "embedded_spec", Source: "asset:swaggerJsonEmbed", Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", FileName: "embedded_spec.go", }, { Name: "server", Source: "asset:serverServer", Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", FileName: "server.go", }, { Name: "builder", Source: "asset:serverBuilder", Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) (toPackagePath .APIPackage) }}", FileName: "{{ snakize (pascalize .Name) }}_api.go", }, { Name: "doc", Source: "asset:serverDoc", Target: "{{ joinFilePath .Target (toPackagePath .ServerPackage) }}", FileName: "doc.go", }, } } } gen.Sections = sec } // TemplateOpts allows type TemplateOpts struct { Name string `mapstructure:"name"` Source string `mapstructure:"source"` Target string `mapstructure:"target"` FileName string `mapstructure:"file_name"` SkipExists bool `mapstructure:"skip_exists"` SkipFormat bool `mapstructure:"skip_format"` } // SectionOpts allows for specifying options to customize the templates used for generation type SectionOpts struct { Application []TemplateOpts `mapstructure:"application"` Operations []TemplateOpts `mapstructure:"operations"` OperationGroups []TemplateOpts `mapstructure:"operation_groups"` Models []TemplateOpts `mapstructure:"models"` } // GenOpts the options for the generator type GenOpts struct { IncludeModel bool IncludeValidator bool IncludeHandler bool IncludeParameters bool IncludeResponses bool IncludeURLBuilder bool IncludeMain bool IncludeSupport bool ExcludeSpec bool DumpData bool ValidateSpec bool FlattenOpts *analysis.FlattenOpts IsClient bool defaultsEnsured bool PropertiesSpecOrder bool StrictAdditionalProperties bool AllowTemplateOverride bool Spec string APIPackage string ModelPackage string ServerPackage string ClientPackage string Principal string Target string Sections SectionOpts LanguageOpts *LanguageOpts TypeMapping map[string]string Imports map[string]string DefaultScheme string DefaultProduces string DefaultConsumes string TemplateDir string Template string RegenerateConfigureAPI bool Operations []string Models []string Tags []string Name string FlagStrategy string CompatibilityMode string ExistingModels string Copyright string } // CheckOpts carries out some global consistency checks on options. // // At the moment, these checks simply protect TargetPath() and SpecPath() // functions. More checks may be added here. func (g *GenOpts) CheckOpts() error { if !filepath.IsAbs(g.Target) { if _, err := filepath.Abs(g.Target); err != nil { return fmt.Errorf("could not locate target %s: %v", g.Target, err) } } if filepath.IsAbs(g.ServerPackage) { return fmt.Errorf("you shouldn't specify an absolute path in --server-package: %s", g.ServerPackage) } if !filepath.IsAbs(g.Spec) && !strings.HasPrefix(g.Spec, "http://") && !strings.HasPrefix(g.Spec, "https://") { if _, err := filepath.Abs(g.Spec); err != nil { return fmt.Errorf("could not locate spec: %s", g.Spec) } } return nil } // TargetPath returns the target generation path relative to the server package. // This method is used by templates, e.g. with {{ .TargetPath }} // // Errors cases are prevented by calling CheckOpts beforehand. // // Example: // Target: ${PWD}/tmp // ServerPackage: abc/efg // // Server is generated in ${PWD}/tmp/abc/efg // relative TargetPath returned: ../../../tmp // func (g *GenOpts) TargetPath() string { var tgt string if g.Target == "" { tgt = "." // That's for windows } else { tgt = g.Target } tgtAbs, _ := filepath.Abs(tgt) srvPkg := filepath.FromSlash(g.LanguageOpts.ManglePackagePath(g.ServerPackage, "server")) srvrAbs := filepath.Join(tgtAbs, srvPkg) tgtRel, _ := filepath.Rel(srvrAbs, filepath.Dir(tgtAbs)) tgtRel = filepath.Join(tgtRel, filepath.Base(tgtAbs)) return tgtRel } // SpecPath returns the path to the spec relative to the server package. // If the spec is remote keep this absolute location. // // If spec is not relative to server (e.g. lives on a different drive on windows), // then the resolved path is absolute. // // This method is used by templates, e.g. with {{ .SpecPath }} // // Errors cases are prevented by calling CheckOpts beforehand. func (g *GenOpts) SpecPath() string { if strings.HasPrefix(g.Spec, "http://") || strings.HasPrefix(g.Spec, "https://") { return g.Spec } // Local specifications specAbs, _ := filepath.Abs(g.Spec) var tgt string if g.Target == "" { tgt = "." // That's for windows } else { tgt = g.Target } tgtAbs, _ := filepath.Abs(tgt) srvPkg := filepath.FromSlash(g.LanguageOpts.ManglePackagePath(g.ServerPackage, "server")) srvAbs := filepath.Join(tgtAbs, srvPkg) specRel, err := filepath.Rel(srvAbs, specAbs) if err != nil { return specAbs } return specRel } // EnsureDefaults for these gen opts func (g *GenOpts) EnsureDefaults() error { if g.defaultsEnsured { return nil } DefaultSectionOpts(g) if g.LanguageOpts == nil { g.LanguageOpts = GoLangOpts() } // set defaults for flattening options g.FlattenOpts = &analysis.FlattenOpts{ Minimal: true, Verbose: true, RemoveUnused: false, Expand: false, } g.defaultsEnsured = true return nil } func (g *GenOpts) location(t *TemplateOpts, data interface{}) (string, string, error) { v := reflect.Indirect(reflect.ValueOf(data)) fld := v.FieldByName("Name") var name string if fld.IsValid() { log.Println("name field", fld.String()) name = fld.String() } fldpack := v.FieldByName("Package") pkg := g.APIPackage if fldpack.IsValid() { log.Println("package field", fldpack.String()) pkg = fldpack.String() } var tags []string tagsF := v.FieldByName("Tags") if tagsF.IsValid() { tags = tagsF.Interface().([]string) } pthTpl, err := template.New(t.Name + "-target").Funcs(FuncMap).Parse(t.Target) if err != nil { return "", "", err } fNameTpl, err := template.New(t.Name + "-filename").Funcs(FuncMap).Parse(t.FileName) if err != nil { return "", "", err } d := struct { Name, Package, APIPackage, ServerPackage, ClientPackage, ModelPackage, Target string Tags []string }{ Name: name, Package: pkg, APIPackage: g.APIPackage, ServerPackage: g.ServerPackage, ClientPackage: g.ClientPackage, ModelPackage: g.ModelPackage, Target: g.Target, Tags: tags, } // pretty.Println(data) var pthBuf bytes.Buffer if e := pthTpl.Execute(&pthBuf, d); e != nil { return "", "", e } var fNameBuf bytes.Buffer if e := fNameTpl.Execute(&fNameBuf, d); e != nil { return "", "", e } return pthBuf.String(), fileName(fNameBuf.String()), nil } func (g *GenOpts) render(t *TemplateOpts, data interface{}) ([]byte, error) { var templ *template.Template if strings.HasPrefix(strings.ToLower(t.Source), "asset:") { tt, err := templates.Get(strings.TrimPrefix(t.Source, "asset:")) if err != nil { return nil, err } templ = tt } if templ == nil { // try to load from repository (and enable dependencies) name := swag.ToJSONName(strings.TrimSuffix(t.Source, ".gotmpl")) tt, err := templates.Get(name) if err == nil { templ = tt } } if templ == nil { // try to load template from disk, in TemplateDir if specified // (dependencies resolution is limited to preloaded assets) var templateFile string if g.TemplateDir != "" { templateFile = filepath.Join(g.TemplateDir, t.Source) } else { templateFile = t.Source } content, err := ioutil.ReadFile(templateFile) if err != nil { return nil, fmt.Errorf("error while opening %s template file: %v", templateFile, err) } tt, err := template.New(t.Source).Funcs(FuncMap).Parse(string(content)) if err != nil { return nil, fmt.Errorf("template parsing failed on template %s: %v", t.Name, err) } templ = tt } if templ == nil { return nil, fmt.Errorf("template %q not found", t.Source) } var tBuf bytes.Buffer if err := templ.Execute(&tBuf, data); err != nil { return nil, fmt.Errorf("template execution failed for template %s: %v", t.Name, err) } log.Printf("executed template %s", t.Source) return tBuf.Bytes(), nil } // Render template and write generated source code // generated code is reformatted ("linted"), which gives an // additional level of checking. If this step fails, the generated // code is still dumped, for template debugging purposes. func (g *GenOpts) write(t *TemplateOpts, data interface{}) error { dir, fname, err := g.location(t, data) if err != nil { return fmt.Errorf("failed to resolve template location for template %s: %v", t.Name, err) } if t.SkipExists && fileExists(dir, fname) { debugLog("skipping generation of %s because it already exists and skip_exist directive is set for %s", filepath.Join(dir, fname), t.Name) return nil } log.Printf("creating generated file %q in %q as %s", fname, dir, t.Name) content, err := g.render(t, data) if err != nil { return fmt.Errorf("failed rendering template data for %s: %v", t.Name, err) } if dir != "" { _, exists := os.Stat(dir) if os.IsNotExist(exists) { debugLog("creating directory %q for \"%s\"", dir, t.Name) // Directory settings consistent with file privileges. // Environment's umask may alter this setup if e := os.MkdirAll(dir, 0755); e != nil { return e } } } // Conditionally format the code, unless the user wants to skip formatted := content var writeerr error if !t.SkipFormat { formatted, err = g.LanguageOpts.FormatContent(fname, content) if err != nil { 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) writeerr = ioutil.WriteFile(filepath.Join(dir, fname), content, 0644) if writeerr != nil { return fmt.Errorf("failed to write (unformatted) file %q in %q: %v", fname, dir, writeerr) } log.Printf("unformatted generated source %q has been dumped for template debugging purposes. DO NOT build on this source!", fname) return fmt.Errorf("source formatting on generated source %q failed: %v", t.Name, err) } } writeerr = ioutil.WriteFile(filepath.Join(dir, fname), formatted, 0644) if writeerr != nil { return fmt.Errorf("failed to write file %q in %q: %v", fname, dir, writeerr) } return err } func fileName(in string) string { ext := filepath.Ext(in) return swag.ToFileName(strings.TrimSuffix(in, ext)) + ext } func (g *GenOpts) shouldRenderApp(t *TemplateOpts, app *GenApp) bool { switch swag.ToFileName(swag.ToGoName(t.Name)) { case "main": return g.IncludeMain case "embedded_spec": return !g.ExcludeSpec default: return true } } func (g *GenOpts) shouldRenderOperations() bool { return g.IncludeHandler || g.IncludeParameters || g.IncludeResponses } func (g *GenOpts) renderApplication(app *GenApp) error { log.Printf("rendering %d templates for application %s", len(g.Sections.Application), app.Name) for _, templ := range g.Sections.Application { if !g.shouldRenderApp(&templ, app) { continue } if err := g.write(&templ, app); err != nil { return err } } return nil } func (g *GenOpts) renderOperationGroup(gg *GenOperationGroup) error { log.Printf("rendering %d templates for operation group %s", len(g.Sections.OperationGroups), g.Name) for _, templ := range g.Sections.OperationGroups { if !g.shouldRenderOperations() { continue } if err := g.write(&templ, gg); err != nil { return err } } return nil } func (g *GenOpts) renderOperation(gg *GenOperation) error { log.Printf("rendering %d templates for operation %s", len(g.Sections.Operations), g.Name) for _, templ := range g.Sections.Operations { if !g.shouldRenderOperations() { continue } if err := g.write(&templ, gg); err != nil { return err } } return nil } func (g *GenOpts) renderDefinition(gg *GenDefinition) error { log.Printf("rendering %d templates for model %s", len(g.Sections.Models), gg.Name) for _, templ := range g.Sections.Models { if !g.IncludeModel { continue } if err := g.write(&templ, gg); err != nil { return err } } return nil } func validateSpec(path string, doc *loads.Document) (err error) { if doc == nil { if path, doc, err = loadSpec(path); err != nil { return err } } result := validate.Spec(doc, strfmt.Default) if result == nil { return nil } str := fmt.Sprintf("The swagger spec at %q is invalid against swagger specification %s. see errors :\n", path, doc.Version()) for _, desc := range result.(*swaggererrors.CompositeError).Errors { str += fmt.Sprintf("- %s\n", desc) } return errors.New(str) } func loadSpec(specFile string) (string, *loads.Document, error) { // find swagger spec document, verify it exists specPath := specFile var err error if !strings.HasPrefix(specPath, "http") { specPath, err = findSwaggerSpec(specFile) if err != nil { return "", nil, err } } // load swagger spec specDoc, err := loads.Spec(specPath) if err != nil { return "", nil, err } return specPath, specDoc, nil } func fileExists(target, name string) bool { _, err := os.Stat(filepath.Join(target, name)) return !os.IsNotExist(err) } func gatherModels(specDoc *loads.Document, modelNames []string) (map[string]spec.Schema, error) { models, mnc := make(map[string]spec.Schema), len(modelNames) defs := specDoc.Spec().Definitions if mnc > 0 { var unknownModels []string for _, m := range modelNames { _, ok := defs[m] if !ok { unknownModels = append(unknownModels, m) } } if len(unknownModels) != 0 { return nil, fmt.Errorf("unknown models: %s", strings.Join(unknownModels, ", ")) } } for k, v := range defs { if mnc == 0 { models[k] = v } for _, nm := range modelNames { if k == nm { models[k] = v } } } return models, nil } func appNameOrDefault(specDoc *loads.Document, name, defaultName string) string { if strings.TrimSpace(name) == "" { if specDoc.Spec().Info != nil && strings.TrimSpace(specDoc.Spec().Info.Title) != "" { name = specDoc.Spec().Info.Title } else { name = defaultName } } return strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(swag.ToGoName(name), "Test"), "API"), "Test") } func containsString(names []string, name string) bool { for _, nm := range names { if nm == name { return true } } return false } type opRef struct { Method string Path string Key string ID string Op *spec.Operation } type opRefs []opRef func (o opRefs) Len() int { return len(o) } func (o opRefs) Swap(i, j int) { o[i], o[j] = o[j], o[i] } func (o opRefs) Less(i, j int) bool { return o[i].Key < o[j].Key } func gatherOperations(specDoc *analysis.Spec, operationIDs []string) map[string]opRef { var oprefs opRefs for method, pathItem := range specDoc.Operations() { for path, operation := range pathItem { // nm := ensureUniqueName(operation.ID, method, path, operations) vv := *operation oprefs = append(oprefs, opRef{ Key: swag.ToGoName(strings.ToLower(method) + " " + path), Method: method, Path: path, ID: vv.ID, Op: &vv, }) } } sort.Sort(oprefs) operations := make(map[string]opRef) for _, opr := range oprefs { nm := opr.ID if nm == "" { nm = opr.Key } oo, found := operations[nm] if found && oo.Method != opr.Method && oo.Path != opr.Path { nm = opr.Key } if len(operationIDs) == 0 || containsString(operationIDs, opr.ID) || containsString(operationIDs, nm) { opr.ID = nm opr.Op.ID = nm operations[nm] = opr } } return operations } func pascalize(arg string) string { runes := []rune(arg) switch len(runes) { case 0: return "" case 1: // handle special case when we have a single rune that is not handled by swag.ToGoName switch runes[0] { case '+', '-', '#', '_': // those cases are handled differently than swag utility return prefixForName(arg) } } return swag.ToGoName(swag.ToGoName(arg)) // want to remove spaces } func prefixForName(arg string) string { first := []rune(arg)[0] if len(arg) == 0 || unicode.IsLetter(first) { return "" } switch first { case '+': return "Plus" case '-': return "Minus" case '#': return "HashTag" // other cases ($,@ etc..) handled by swag.ToGoName } return "Nr" } func init() { // this makes the ToGoName func behave with the special // prefixing rule above swag.GoNamePrefixFunc = prefixForName } func pruneEmpty(in []string) (out []string) { for _, v := range in { if v != "" { out = append(out, v) } } return } func trimBOM(in string) string { return strings.Trim(in, "\xef\xbb\xbf") } func validateAndFlattenSpec(opts *GenOpts, specDoc *loads.Document) (*loads.Document, error) { var err error // Validate if needed if opts.ValidateSpec { log.Printf("validating spec %v", opts.Spec) if erv := validateSpec(opts.Spec, specDoc); erv != nil { return specDoc, erv } } // Restore spec to original opts.Spec, specDoc, err = loadSpec(opts.Spec) if err != nil { return nil, err } absBasePath := specDoc.SpecFilePath() if !filepath.IsAbs(absBasePath) { cwd, _ := os.Getwd() absBasePath = filepath.Join(cwd, absBasePath) } // Some preprocessing is required before codegen // // This ensures at least that $ref's in the spec document are canonical, // i.e all $ref are local to this file and point to some uniquely named definition. // // Default option is to ensure minimal flattening of $ref, bundling remote $refs and relocating arbitrary JSON // pointers as definitions. // This preprocessing may introduce duplicate names (e.g. remote $ref with same name). In this case, a definition // suffixed with "OAIGen" is produced. // // Full flattening option farther transforms the spec by moving every complex object (e.g. with some properties) // as a standalone definition. // // Eventually, an "expand spec" option is available. It is essentially useful for testing purposes. // // NOTE(fredbi): spec expansion may produce some unsupported constructs and is not yet protected against the // following cases: // - polymorphic types generation may fail with expansion (expand destructs the reuse intent of the $ref in allOf) // - name duplicates may occur and result in compilation failures // The right place to fix these shortcomings is go-openapi/analysis. opts.FlattenOpts.BasePath = absBasePath // BasePath must be absolute opts.FlattenOpts.Spec = analysis.New(specDoc.Spec()) var preprocessingOption string switch { case opts.FlattenOpts.Expand: preprocessingOption = "expand" case opts.FlattenOpts.Minimal: preprocessingOption = "minimal flattening" default: preprocessingOption = "full flattening" } log.Printf("preprocessing spec with option: %s", preprocessingOption) if err = analysis.Flatten(*opts.FlattenOpts); err != nil { return nil, err } // yields the preprocessed spec document return specDoc, nil } // gatherSecuritySchemes produces a sorted representation from a map of spec security schemes func gatherSecuritySchemes(securitySchemes map[string]spec.SecurityScheme, appName, principal, receiver string) (security GenSecuritySchemes) { for scheme, req := range securitySchemes { isOAuth2 := strings.ToLower(req.Type) == "oauth2" var scopes []string if isOAuth2 { for k := range req.Scopes { scopes = append(scopes, k) } } sort.Strings(scopes) security = append(security, GenSecurityScheme{ AppName: appName, ID: scheme, ReceiverName: receiver, Name: req.Name, IsBasicAuth: strings.ToLower(req.Type) == "basic", IsAPIKeyAuth: strings.ToLower(req.Type) == "apikey", IsOAuth2: isOAuth2, Scopes: scopes, Principal: principal, Source: req.In, // from original spec Description: req.Description, Type: strings.ToLower(req.Type), In: req.In, Flow: req.Flow, AuthorizationURL: req.AuthorizationURL, TokenURL: req.TokenURL, Extensions: req.Extensions, }) } sort.Sort(security) return } // gatherExtraSchemas produces a sorted list of extra schemas. // // ExtraSchemas are inlined types rendered in the same model file. func gatherExtraSchemas(extraMap map[string]GenSchema) (extras GenSchemaList) { var extraKeys []string for k := range extraMap { extraKeys = append(extraKeys, k) } sort.Strings(extraKeys) for _, k := range extraKeys { // figure out if top level validations are needed p := extraMap[k] p.HasValidations = shallowValidationLookup(p) extras = append(extras, p) } return } func sharedValidationsFromSimple(v spec.CommonValidations, isRequired bool) (sh sharedValidations) { sh = sharedValidations{ Required: isRequired, Maximum: v.Maximum, ExclusiveMaximum: v.ExclusiveMaximum, Minimum: v.Minimum, ExclusiveMinimum: v.ExclusiveMinimum, MaxLength: v.MaxLength, MinLength: v.MinLength, Pattern: v.Pattern, MaxItems: v.MaxItems, MinItems: v.MinItems, UniqueItems: v.UniqueItems, MultipleOf: v.MultipleOf, Enum: v.Enum, } return } func sharedValidationsFromSchema(v spec.Schema, isRequired bool) (sh sharedValidations) { sh = sharedValidations{ Required: isRequired, Maximum: v.Maximum, ExclusiveMaximum: v.ExclusiveMaximum, Minimum: v.Minimum, ExclusiveMinimum: v.ExclusiveMinimum, MaxLength: v.MaxLength, MinLength: v.MinLength, Pattern: v.Pattern, MaxItems: v.MaxItems, MinItems: v.MinItems, UniqueItems: v.UniqueItems, MultipleOf: v.MultipleOf, Enum: v.Enum, } return }