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