github.com/shogo82148/goa-v1@v1.6.2/goagen/gen_app/generator.go (about)

     1  package genapp
     2  
     3  import (
     4  	"flag"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  
    10  	"github.com/shogo82148/goa-v1/design"
    11  	"github.com/shogo82148/goa-v1/goagen/codegen"
    12  	"github.com/shogo82148/goa-v1/goagen/utils"
    13  )
    14  
    15  //NewGenerator returns an initialized instance of an Application Generator
    16  func NewGenerator(options ...Option) *Generator {
    17  	g := &Generator{}
    18  	g.validator = codegen.NewValidator()
    19  
    20  	for _, option := range options {
    21  		option(g)
    22  	}
    23  
    24  	return g
    25  }
    26  
    27  // Generator is the application code generator.
    28  type Generator struct {
    29  	API       *design.APIDefinition // The API definition
    30  	OutDir    string                // Path to output directory
    31  	Target    string                // Name of generated package
    32  	NoTest    bool                  // Whether to skip test generation
    33  	genfiles  []string              // Generated files
    34  	validator *codegen.Validator    // Validation code generator
    35  }
    36  
    37  // Generate is the generator entry point called by the meta generator.
    38  func Generate() (files []string, err error) {
    39  	var (
    40  		outDir, toolDir, target, ver string
    41  		notest, notool, regen        bool
    42  	)
    43  
    44  	set := flag.NewFlagSet("app", flag.PanicOnError)
    45  	set.String("design", "", "")
    46  	set.StringVar(&outDir, "out", "", "")
    47  	set.StringVar(&target, "pkg", "app", "")
    48  	set.StringVar(&ver, "version", "", "")
    49  	set.StringVar(&toolDir, "tooldir", "tool", "")
    50  	set.BoolVar(&notest, "notest", false, "")
    51  	set.BoolVar(&notool, "notool", false, "")
    52  	set.BoolVar(&regen, "regen", false, "")
    53  	set.Bool("force", false, "")
    54  	set.Parse(os.Args[1:])
    55  	outDir = filepath.Join(outDir, target)
    56  
    57  	if err := codegen.CheckVersion(ver); err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	target = codegen.Goify(target, false)
    62  	g := &Generator{OutDir: outDir, Target: target, NoTest: notest, API: design.Design, validator: codegen.NewValidator()}
    63  
    64  	return g.Generate()
    65  }
    66  
    67  // Generate the application code, implement codegen.Generator.
    68  func (g *Generator) Generate() (_ []string, err error) {
    69  	if g.API == nil {
    70  		return nil, fmt.Errorf("missing API definition, make sure design is properly initialized")
    71  	}
    72  
    73  	go utils.Catch(nil, func() { g.Cleanup() })
    74  
    75  	defer func() {
    76  		if err != nil {
    77  			g.Cleanup()
    78  		}
    79  	}()
    80  
    81  	codegen.Reserved[g.Target] = true
    82  
    83  	os.RemoveAll(g.OutDir)
    84  
    85  	if err := os.MkdirAll(g.OutDir, 0755); err != nil {
    86  		return nil, err
    87  	}
    88  	g.genfiles = []string{g.OutDir}
    89  	if err := g.generateContexts(); err != nil {
    90  		return nil, err
    91  	}
    92  	if err := g.generateControllers(); err != nil {
    93  		return nil, err
    94  	}
    95  	if err := g.generateSecurity(); err != nil {
    96  		return nil, err
    97  	}
    98  	if err := g.generateHrefs(); err != nil {
    99  		return nil, err
   100  	}
   101  	if err := g.generateMediaTypes(); err != nil {
   102  		return nil, err
   103  	}
   104  	if err := g.generateUserTypes(); err != nil {
   105  		return nil, err
   106  	}
   107  	if !g.NoTest {
   108  		if err := g.generateResourceTest(); err != nil {
   109  			return nil, err
   110  		}
   111  	}
   112  
   113  	return g.genfiles, nil
   114  }
   115  
   116  // Cleanup removes the entire "app" directory if it was created by this generator.
   117  func (g *Generator) Cleanup() {
   118  	if len(g.genfiles) == 0 {
   119  		return
   120  	}
   121  	os.RemoveAll(g.OutDir)
   122  	g.genfiles = nil
   123  }
   124  
   125  // generateContexts iterates through the API resources and actions and generates the action
   126  // contexts.
   127  func (g *Generator) generateContexts() (err error) {
   128  	var (
   129  		ctxFile string
   130  		ctxWr   *ContextsWriter
   131  	)
   132  	{
   133  		ctxFile = filepath.Join(g.OutDir, "contexts.go")
   134  		ctxWr, err = NewContextsWriter(ctxFile)
   135  		if err != nil {
   136  			return
   137  		}
   138  	}
   139  	defer func() {
   140  		ctxWr.Close()
   141  		if err == nil {
   142  			err = ctxWr.FormatCode()
   143  		}
   144  	}()
   145  	title := fmt.Sprintf("%s: Application Contexts", g.API.Context())
   146  	imports := []*codegen.ImportSpec{
   147  		codegen.SimpleImport("fmt"),
   148  		codegen.SimpleImport("net/http"),
   149  		codegen.SimpleImport("strconv"),
   150  		codegen.SimpleImport("strings"),
   151  		codegen.SimpleImport("time"),
   152  		codegen.SimpleImport("unicode/utf8"),
   153  		codegen.NewImport("goa", "github.com/shogo82148/goa-v1"),
   154  		codegen.NewImport("uuid", "github.com/gofrs/uuid"),
   155  		codegen.SimpleImport("context"),
   156  	}
   157  	g.API.IterateResources(func(r *design.ResourceDefinition) error {
   158  		return r.IterateActions(func(a *design.ActionDefinition) error {
   159  			if a.Payload != nil {
   160  				imports = codegen.AttributeImports(a.Payload.AttributeDefinition, imports, nil)
   161  			}
   162  			return nil
   163  		})
   164  	})
   165  
   166  	g.genfiles = append(g.genfiles, ctxFile)
   167  	if err = ctxWr.WriteHeader(title, g.Target, imports); err != nil {
   168  		return
   169  	}
   170  	err = g.API.IterateResources(func(r *design.ResourceDefinition) error {
   171  		return r.IterateActions(func(a *design.ActionDefinition) error {
   172  			ctxName := codegen.Goify(a.Name, true) + codegen.Goify(a.Parent.Name, true) + "Context"
   173  			headers := &design.AttributeDefinition{
   174  				Type: design.Object{},
   175  			}
   176  			if r.Headers != nil {
   177  				headers.Merge(r.Headers)
   178  				headers.Validation = r.Headers.Validation
   179  			}
   180  			if a.Headers != nil {
   181  				headers.Merge(a.Headers)
   182  				headers.Validation = a.Headers.Validation
   183  			}
   184  			if headers != nil && len(headers.Type.ToObject()) == 0 {
   185  				headers = nil // So that {{if .Headers}} returns false in templates
   186  			}
   187  			params := a.AllParams()
   188  			if params != nil && len(params.Type.ToObject()) == 0 {
   189  				params = nil // So that {{if .Params}} returns false in templates
   190  			}
   191  
   192  			non101 := make(map[string]*design.ResponseDefinition)
   193  			for k, v := range a.Responses {
   194  				if v.Status != 101 {
   195  					non101[k] = v
   196  				}
   197  			}
   198  			ctxData := ContextTemplateData{
   199  				Name:         ctxName,
   200  				ResourceName: r.Name,
   201  				ActionName:   a.Name,
   202  				Payload:      a.Payload,
   203  				Params:       params,
   204  				Headers:      headers,
   205  				Routes:       a.Routes,
   206  				Responses:    non101,
   207  				API:          g.API,
   208  				DefaultPkg:   g.Target,
   209  				Security:     a.Security,
   210  			}
   211  			return ctxWr.Execute(&ctxData)
   212  		})
   213  	})
   214  	return
   215  }
   216  
   217  // generateControllers iterates through the API resources and generates the low level
   218  // controllers.
   219  func (g *Generator) generateControllers() (err error) {
   220  	var (
   221  		ctlFile string
   222  		ctlWr   *ControllersWriter
   223  	)
   224  	{
   225  		ctlFile = filepath.Join(g.OutDir, "controllers.go")
   226  		ctlWr, err = NewControllersWriter(ctlFile)
   227  		if err != nil {
   228  			return
   229  		}
   230  	}
   231  	defer func() {
   232  		ctlWr.Close()
   233  		if err == nil {
   234  			err = ctlWr.FormatCode()
   235  		}
   236  	}()
   237  	title := fmt.Sprintf("%s: Application Controllers", g.API.Context())
   238  	imports := []*codegen.ImportSpec{
   239  		codegen.SimpleImport("net/http"),
   240  		codegen.SimpleImport("fmt"),
   241  		codegen.SimpleImport("context"),
   242  		codegen.NewImport("goa", "github.com/shogo82148/goa-v1"),
   243  		codegen.SimpleImport("github.com/shogo82148/goa-v1/cors"),
   244  		codegen.SimpleImport("regexp"),
   245  		codegen.SimpleImport("strconv"),
   246  		codegen.SimpleImport("time"),
   247  		codegen.NewImport("uuid", "github.com/gofrs/uuid"),
   248  		codegen.SimpleImport("errors"),
   249  	}
   250  	encoders, err := BuildEncoders(g.API.Produces, true)
   251  	if err != nil {
   252  		return err
   253  	}
   254  	decoders, err := BuildEncoders(g.API.Consumes, false)
   255  	if err != nil {
   256  		return err
   257  	}
   258  	encoderImports := make(map[string]bool)
   259  	for _, data := range encoders {
   260  		encoderImports[data.PackagePath] = true
   261  	}
   262  	for _, data := range decoders {
   263  		encoderImports[data.PackagePath] = true
   264  	}
   265  	var packagePaths []string
   266  	for packagePath := range encoderImports {
   267  		if packagePath != "github.com/shogo82148/goa-v1" {
   268  			packagePaths = append(packagePaths, packagePath)
   269  		}
   270  	}
   271  	sort.Strings(packagePaths)
   272  	for _, packagePath := range packagePaths {
   273  		imports = append(imports, codegen.SimpleImport(packagePath))
   274  	}
   275  	if err = ctlWr.WriteHeader(title, g.Target, imports); err != nil {
   276  		return err
   277  	}
   278  	if err = ctlWr.WriteInitService(encoders, decoders); err != nil {
   279  		return err
   280  	}
   281  
   282  	g.genfiles = append(g.genfiles, ctlFile)
   283  	var controllersData []*ControllerTemplateData
   284  	g.API.IterateResources(func(r *design.ResourceDefinition) error {
   285  		// Create file servers for all directory file servers that serve index.html.
   286  		fileServers := r.FileServers
   287  		for _, fs := range r.FileServers {
   288  			if fs.IsDir() {
   289  				rpath := design.WildcardRegex.ReplaceAllLiteralString(fs.RequestPath, "")
   290  				rpath += "/"
   291  				fileServers = append(fileServers, &design.FileServerDefinition{
   292  					Parent:      fs.Parent,
   293  					Description: fs.Description,
   294  					Docs:        fs.Docs,
   295  					FilePath:    filepath.Join(fs.FilePath, "index.html"),
   296  					RequestPath: rpath,
   297  					Metadata:    fs.Metadata,
   298  					Security:    fs.Security,
   299  				})
   300  			}
   301  		}
   302  		data := &ControllerTemplateData{
   303  			API:            g.API,
   304  			Resource:       codegen.Goify(r.Name, true),
   305  			PreflightPaths: r.PreflightPaths(),
   306  			FileServers:    fileServers,
   307  		}
   308  		r.IterateActions(func(a *design.ActionDefinition) error {
   309  			context := fmt.Sprintf("%s%sContext", codegen.Goify(a.Name, true), codegen.Goify(r.Name, true))
   310  			unmarshal := fmt.Sprintf("unmarshal%s%sPayload", codegen.Goify(a.Name, true), codegen.Goify(r.Name, true))
   311  			action := map[string]interface{}{
   312  				"Name":             codegen.Goify(a.Name, true),
   313  				"DesignName":       a.Name,
   314  				"Routes":           a.Routes,
   315  				"Context":          context,
   316  				"Unmarshal":        unmarshal,
   317  				"Payload":          a.Payload,
   318  				"PayloadOptional":  a.PayloadOptional,
   319  				"PayloadMultipart": a.PayloadMultipart,
   320  				"Security":         a.Security,
   321  			}
   322  			data.Actions = append(data.Actions, action)
   323  			return nil
   324  		})
   325  		if len(data.Actions) > 0 || len(data.FileServers) > 0 {
   326  			data.Encoders = encoders
   327  			data.Decoders = decoders
   328  			data.Origins = r.AllOrigins()
   329  			controllersData = append(controllersData, data)
   330  		}
   331  		return nil
   332  	})
   333  	err = ctlWr.Execute(controllersData)
   334  	return
   335  }
   336  
   337  // generateControllers iterates through the API resources and generates the low level
   338  // controllers.
   339  func (g *Generator) generateSecurity() (err error) {
   340  	if len(g.API.SecuritySchemes) == 0 {
   341  		return nil
   342  	}
   343  
   344  	var (
   345  		secFile string
   346  		secWr   *SecurityWriter
   347  	)
   348  	{
   349  		secFile = filepath.Join(g.OutDir, "security.go")
   350  		secWr, err = NewSecurityWriter(secFile)
   351  		if err != nil {
   352  			return
   353  		}
   354  	}
   355  	defer func() {
   356  		secWr.Close()
   357  		if err == nil {
   358  			err = secWr.FormatCode()
   359  		}
   360  	}()
   361  	title := fmt.Sprintf("%s: Application Security", g.API.Context())
   362  	imports := []*codegen.ImportSpec{
   363  		codegen.SimpleImport("net/http"),
   364  		codegen.SimpleImport("errors"),
   365  		codegen.SimpleImport("context"),
   366  		codegen.NewImport("goa", "github.com/shogo82148/goa-v1"),
   367  	}
   368  	if err = secWr.WriteHeader(title, g.Target, imports); err != nil {
   369  		return err
   370  	}
   371  	g.genfiles = append(g.genfiles, secFile)
   372  	err = secWr.Execute(design.Design.SecuritySchemes)
   373  
   374  	return
   375  }
   376  
   377  // generateHrefs iterates through the API resources and generates the href factory methods.
   378  func (g *Generator) generateHrefs() (err error) {
   379  	var (
   380  		hrefFile string
   381  		resWr    *ResourcesWriter
   382  	)
   383  	{
   384  		hrefFile = filepath.Join(g.OutDir, "hrefs.go")
   385  		resWr, err = NewResourcesWriter(hrefFile)
   386  		if err != nil {
   387  			return
   388  		}
   389  	}
   390  	defer func() {
   391  		resWr.Close()
   392  		if err == nil {
   393  			err = resWr.FormatCode()
   394  		}
   395  	}()
   396  	title := fmt.Sprintf("%s: Application Resource Href Factories", g.API.Context())
   397  	imports := []*codegen.ImportSpec{
   398  		codegen.SimpleImport("fmt"),
   399  		codegen.SimpleImport("strings"),
   400  	}
   401  	if err = resWr.WriteHeader(title, g.Target, imports); err != nil {
   402  		return err
   403  	}
   404  	g.genfiles = append(g.genfiles, hrefFile)
   405  	err = g.API.IterateResources(func(r *design.ResourceDefinition) error {
   406  		m := g.API.MediaTypeWithIdentifier(r.MediaType)
   407  		var identifier string
   408  		if m != nil {
   409  			identifier = m.Identifier
   410  		} else {
   411  			identifier = "text/plain"
   412  		}
   413  		data := ResourceData{
   414  			Name:              codegen.Goify(r.Name, true),
   415  			Identifier:        identifier,
   416  			Description:       r.Description,
   417  			Type:              m,
   418  			CanonicalTemplate: codegen.CanonicalTemplate(r),
   419  			CanonicalParams:   codegen.CanonicalParams(r),
   420  		}
   421  		return resWr.Execute(&data)
   422  	})
   423  	return
   424  }
   425  
   426  // generateMediaTypes iterates through the media types and generate the data structures and
   427  // marshaling code.
   428  func (g *Generator) generateMediaTypes() (err error) {
   429  	var (
   430  		mtFile string
   431  		mtWr   *MediaTypesWriter
   432  	)
   433  	{
   434  		mtFile = filepath.Join(g.OutDir, "media_types.go")
   435  		mtWr, err = NewMediaTypesWriter(mtFile)
   436  		if err != nil {
   437  			return
   438  		}
   439  	}
   440  	defer func() {
   441  		mtWr.Close()
   442  		if err == nil {
   443  			err = mtWr.FormatCode()
   444  		}
   445  	}()
   446  	title := fmt.Sprintf("%s: Application Media Types", g.API.Context())
   447  	imports := []*codegen.ImportSpec{
   448  		codegen.NewImport("goa", "github.com/shogo82148/goa-v1"),
   449  		codegen.SimpleImport("fmt"),
   450  		codegen.SimpleImport("time"),
   451  		codegen.SimpleImport("unicode/utf8"),
   452  		codegen.NewImport("uuid", "github.com/gofrs/uuid"),
   453  	}
   454  	for _, v := range g.API.MediaTypes {
   455  		imports = codegen.AttributeImports(v.AttributeDefinition, imports, nil)
   456  	}
   457  	if err = mtWr.WriteHeader(title, g.Target, imports); err != nil {
   458  		return err
   459  	}
   460  	g.genfiles = append(g.genfiles, mtFile)
   461  	err = g.API.IterateMediaTypes(func(mt *design.MediaTypeDefinition) error {
   462  		if mt.IsError() {
   463  			return nil
   464  		}
   465  		if mt.Type.IsObject() || mt.Type.IsArray() {
   466  			return mtWr.Execute(mt)
   467  		}
   468  		return nil
   469  	})
   470  	return
   471  }
   472  
   473  // generateUserTypes iterates through the user types and generates the data structures and
   474  // marshaling code.
   475  func (g *Generator) generateUserTypes() (err error) {
   476  	var (
   477  		utFile string
   478  		utWr   *UserTypesWriter
   479  	)
   480  	{
   481  		utFile = filepath.Join(g.OutDir, "user_types.go")
   482  		utWr, err = NewUserTypesWriter(utFile)
   483  		if err != nil {
   484  			return
   485  		}
   486  	}
   487  	defer func() {
   488  		utWr.Close()
   489  		if err == nil {
   490  			err = utWr.FormatCode()
   491  		}
   492  	}()
   493  	title := fmt.Sprintf("%s: Application User Types", g.API.Context())
   494  	imports := []*codegen.ImportSpec{
   495  		codegen.SimpleImport("fmt"),
   496  		codegen.SimpleImport("mime/multipart"),
   497  		codegen.SimpleImport("time"),
   498  		codegen.SimpleImport("unicode/utf8"),
   499  		codegen.NewImport("goa", "github.com/shogo82148/goa-v1"),
   500  		codegen.NewImport("uuid", "github.com/gofrs/uuid"),
   501  	}
   502  	for _, v := range g.API.Types {
   503  		imports = codegen.AttributeImports(v.AttributeDefinition, imports, nil)
   504  	}
   505  	if err = utWr.WriteHeader(title, g.Target, imports); err != nil {
   506  		return err
   507  	}
   508  	g.genfiles = append(g.genfiles, utFile)
   509  	err = g.API.IterateUserTypes(func(t *design.UserTypeDefinition) error {
   510  		return utWr.Execute(t)
   511  	})
   512  	return
   513  }