github.com/blp1526/goa@v1.4.0/goagen/gen_main/generator.go (about)

     1  package genmain
     2  
     3  import (
     4  	"bufio"
     5  	"flag"
     6  	"fmt"
     7  	"go/ast"
     8  	"go/parser"
     9  	"go/token"
    10  	"net"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"regexp"
    15  	"strings"
    16  	"text/template"
    17  
    18  	"github.com/goadesign/goa/design"
    19  	"github.com/goadesign/goa/goagen/codegen"
    20  	"github.com/goadesign/goa/goagen/utils"
    21  )
    22  
    23  //NewGenerator returns an initialized instance of a JavaScript Client Generator
    24  func NewGenerator(options ...Option) *Generator {
    25  	g := &Generator{OutDir: "."}
    26  
    27  	for _, option := range options {
    28  		option(g)
    29  	}
    30  
    31  	return g
    32  }
    33  
    34  // Generator is the application code generator.
    35  type Generator struct {
    36  	API       *design.APIDefinition // The API definition
    37  	OutDir    string                // Path to output directory
    38  	DesignPkg string                // Path to design package, only used to mark generated files.
    39  	Target    string                // Name of generated "app" package
    40  	Force     bool                  // Whether to override existing files
    41  	Regen     bool                  // Whether to regenerate scaffolding in place, maintaining controller implementation
    42  	genfiles  []string              // Generated files
    43  }
    44  
    45  // Generate is the generator entry point called by the meta generator.
    46  func Generate() (files []string, err error) {
    47  	var (
    48  		outDir, toolDir, designPkg, target, ver string
    49  		force, notool, regen                    bool
    50  	)
    51  
    52  	set := flag.NewFlagSet("main", flag.PanicOnError)
    53  	set.StringVar(&outDir, "out", "", "")
    54  	set.StringVar(&designPkg, "design", "", "")
    55  	set.StringVar(&target, "pkg", "app", "")
    56  	set.StringVar(&ver, "version", "", "")
    57  	set.StringVar(&toolDir, "tooldir", "tool", "")
    58  	set.BoolVar(&notool, "notool", false, "")
    59  	set.BoolVar(&force, "force", false, "")
    60  	set.BoolVar(&regen, "regen", false, "")
    61  	set.Bool("notest", false, "")
    62  	set.Parse(os.Args[1:])
    63  
    64  	if err := codegen.CheckVersion(ver); err != nil {
    65  		return nil, err
    66  	}
    67  
    68  	target = codegen.Goify(target, false)
    69  	g := &Generator{OutDir: outDir, DesignPkg: designPkg, Target: target, Force: force, Regen: regen, API: design.Design}
    70  
    71  	return g.Generate()
    72  }
    73  
    74  func extractControllerBody(filename string) (map[string]string, []*ast.ImportSpec, error) {
    75  	// First check if a file is there. If not, return empty results to let generation proceed.
    76  	if _, e := os.Stat(filename); e != nil {
    77  		return map[string]string{}, []*ast.ImportSpec{}, nil
    78  	}
    79  	fset := token.NewFileSet()
    80  	pfile, err := parser.ParseFile(fset, filename, nil, parser.ImportsOnly)
    81  	if err != nil {
    82  		return nil, nil, err
    83  	}
    84  	f, err := os.Open(filename)
    85  	if err != nil {
    86  		return nil, nil, err
    87  	}
    88  	defer f.Close()
    89  	var (
    90  		inBlock bool
    91  		block   []string
    92  	)
    93  	actionImpls := map[string]string{}
    94  	scanner := bufio.NewScanner(f)
    95  	for scanner.Scan() {
    96  		line := scanner.Text()
    97  		match := linePattern.FindStringSubmatch(line)
    98  		if len(match) == 3 {
    99  			switch match[2] {
   100  			case "start":
   101  				inBlock = true
   102  			case "end":
   103  				inBlock = false
   104  				actionImpls[match[1]] = strings.Join(block, "\n")
   105  				block = []string{}
   106  			}
   107  			continue
   108  		}
   109  		if inBlock {
   110  			block = append(block, line)
   111  		}
   112  	}
   113  	if err := scanner.Err(); err != nil {
   114  		return nil, nil, err
   115  	}
   116  	return actionImpls, pfile.Imports, nil
   117  }
   118  
   119  // GenerateController generates the controller corresponding to the given
   120  // resource and returns the generated filename.
   121  func GenerateController(force, regen bool, appPkg, outDir, pkg, name string, r *design.ResourceDefinition) (filename string, err error) {
   122  	filename = filepath.Join(outDir, codegen.SnakeCase(name)+".go")
   123  	var (
   124  		actionImpls      map[string]string
   125  		extractedImports []*ast.ImportSpec
   126  	)
   127  	if regen {
   128  		actionImpls, extractedImports, err = extractControllerBody(filename)
   129  		if err != nil {
   130  			return "", err
   131  		}
   132  		os.Remove(filename)
   133  	}
   134  	if force {
   135  		os.Remove(filename)
   136  	}
   137  	if _, e := os.Stat(filename); e == nil {
   138  		return "", nil
   139  	}
   140  	if err = os.MkdirAll(outDir, 0755); err != nil {
   141  		return "", err
   142  	}
   143  
   144  	var file *codegen.SourceFile
   145  	file, err = codegen.SourceFileFor(filename)
   146  	if err != nil {
   147  		return "", err
   148  	}
   149  	defer func() {
   150  		file.Close()
   151  		if err == nil {
   152  			err = file.FormatCode()
   153  		}
   154  	}()
   155  
   156  	elems := strings.Split(appPkg, "/")
   157  	pkgName := elems[len(elems)-1]
   158  	var imp string
   159  	if _, err := codegen.PackageSourcePath(appPkg); err == nil {
   160  		imp = appPkg
   161  	} else {
   162  		imp, err = codegen.PackagePath(outDir)
   163  		if err != nil {
   164  			return "", err
   165  		}
   166  		imp = path.Join(filepath.ToSlash(imp), appPkg)
   167  	}
   168  
   169  	imports := []*codegen.ImportSpec{
   170  		codegen.SimpleImport("io"),
   171  		codegen.SimpleImport("github.com/goadesign/goa"),
   172  		codegen.SimpleImport(imp),
   173  		codegen.SimpleImport("golang.org/x/net/websocket"),
   174  	}
   175  	for _, imp := range extractedImports {
   176  		// This may introduce duplicate imports of the defaults, but
   177  		// that'll get worked out by Format later.
   178  		var cgimp *codegen.ImportSpec
   179  		path := strings.Trim(imp.Path.Value, `"`)
   180  		if imp.Name != nil {
   181  			cgimp = codegen.NewImport(imp.Name.Name, path)
   182  		} else {
   183  			cgimp = codegen.SimpleImport(path)
   184  		}
   185  		imports = append(imports, cgimp)
   186  	}
   187  
   188  	funcs := funcMap(pkgName, actionImpls)
   189  	if err = file.WriteHeader("", pkg, imports); err != nil {
   190  		return "", err
   191  	}
   192  	if err = file.ExecuteTemplate("controller", ctrlT, funcs, r); err != nil {
   193  		return "", err
   194  	}
   195  	err = r.IterateActions(func(a *design.ActionDefinition) error {
   196  		if a.WebSocket() {
   197  			return file.ExecuteTemplate("actionWS", actionWST, funcs, a)
   198  		}
   199  		return file.ExecuteTemplate("action", actionT, funcs, a)
   200  	})
   201  	if err != nil {
   202  		return "", err
   203  	}
   204  	return
   205  }
   206  
   207  // Generate produces the skeleton main.
   208  func (g *Generator) Generate() (_ []string, err error) {
   209  	if g.API == nil {
   210  		return nil, fmt.Errorf("missing API definition, make sure design is properly initialized")
   211  	}
   212  
   213  	go utils.Catch(nil, func() { g.Cleanup() })
   214  
   215  	defer func() {
   216  		if err != nil {
   217  			g.Cleanup()
   218  		}
   219  	}()
   220  
   221  	if g.Target == "" {
   222  		g.Target = "app"
   223  	}
   224  
   225  	codegen.Reserved[g.Target] = true
   226  
   227  	mainFile := filepath.Join(g.OutDir, "main.go")
   228  	if g.Force {
   229  		os.Remove(mainFile)
   230  	}
   231  	_, err = os.Stat(mainFile)
   232  	if err != nil {
   233  		// ensure that the output directory exists before creating a new main
   234  		if err = os.MkdirAll(g.OutDir, 0755); err != nil {
   235  			return nil, err
   236  		}
   237  		if err = g.createMainFile(mainFile, funcMap(g.Target, nil)); err != nil {
   238  			return nil, err
   239  		}
   240  	}
   241  
   242  	err = g.API.IterateResources(func(r *design.ResourceDefinition) error {
   243  		filename, err := GenerateController(g.Force, g.Regen, g.Target, g.OutDir, "main", r.Name, r)
   244  		if err != nil {
   245  			return err
   246  		}
   247  
   248  		g.genfiles = append(g.genfiles, filename)
   249  		return nil
   250  	})
   251  	if err != nil {
   252  		return
   253  	}
   254  
   255  	return g.genfiles, nil
   256  }
   257  
   258  // Cleanup removes all the files generated by this generator during the last invokation of Generate.
   259  func (g *Generator) Cleanup() {
   260  	for _, f := range g.genfiles {
   261  		os.Remove(f)
   262  	}
   263  	g.genfiles = nil
   264  }
   265  
   266  func (g *Generator) createMainFile(mainFile string, funcs template.FuncMap) (err error) {
   267  	var file *codegen.SourceFile
   268  	file, err = codegen.SourceFileFor(mainFile)
   269  	if err != nil {
   270  		return err
   271  	}
   272  	defer func() {
   273  		file.Close()
   274  		if err == nil {
   275  			err = file.FormatCode()
   276  		}
   277  	}()
   278  	g.genfiles = append(g.genfiles, mainFile)
   279  	funcs["getPort"] = func(hostport string) string {
   280  		_, port, err := net.SplitHostPort(hostport)
   281  		if err != nil {
   282  			return "8080"
   283  		}
   284  		return port
   285  	}
   286  	outPkg, err := codegen.PackagePath(g.OutDir)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	appPkg := path.Join(outPkg, "app")
   291  	imports := []*codegen.ImportSpec{
   292  		codegen.SimpleImport("time"),
   293  		codegen.SimpleImport("github.com/goadesign/goa"),
   294  		codegen.SimpleImport("github.com/goadesign/goa/middleware"),
   295  		codegen.SimpleImport(appPkg),
   296  	}
   297  	file.Write([]byte("//go:generate goagen bootstrap -d " + g.DesignPkg + "\n\n"))
   298  	if err = file.WriteHeader("", "main", imports); err != nil {
   299  		return err
   300  	}
   301  	data := map[string]interface{}{
   302  		"Name": g.API.Name,
   303  		"API":  g.API,
   304  	}
   305  	err = file.ExecuteTemplate("main", mainT, funcs, data)
   306  	return
   307  }
   308  
   309  // tempCount is the counter used to create unique temporary variable names.
   310  var tempCount int
   311  
   312  // tempvar generates a unique temp var name.
   313  func tempvar() string {
   314  	tempCount++
   315  	if tempCount == 1 {
   316  		return "c"
   317  	}
   318  	return fmt.Sprintf("c%d", tempCount)
   319  }
   320  
   321  func okResp(a *design.ActionDefinition, appPkg string) map[string]interface{} {
   322  	var ok *design.ResponseDefinition
   323  	for _, resp := range a.Responses {
   324  		if resp.Status == 200 {
   325  			ok = resp
   326  			break
   327  		}
   328  	}
   329  	if ok == nil {
   330  		return nil
   331  	}
   332  	var mt *design.MediaTypeDefinition
   333  	var ok2 bool
   334  	if mt, ok2 = design.Design.MediaTypes[design.CanonicalIdentifier(ok.MediaType)]; !ok2 {
   335  		return nil
   336  	}
   337  	view := ok.ViewName
   338  	if view == "" {
   339  		view = design.DefaultView
   340  	}
   341  	pmt, _, err := mt.Project(view)
   342  	if err != nil {
   343  		return nil
   344  	}
   345  	var typeref string
   346  	if pmt.IsError() {
   347  		typeref = `goa.ErrInternal("not implemented")`
   348  	} else {
   349  		name := codegen.GoTypeRef(pmt, pmt.AllRequired(), 1, false)
   350  		var pointer string
   351  		if strings.HasPrefix(name, "*") {
   352  			name = name[1:]
   353  			pointer = "*"
   354  		}
   355  		typeref = fmt.Sprintf("%s%s.%s", pointer, appPkg, name)
   356  		if strings.HasPrefix(typeref, "*") {
   357  			typeref = "&" + typeref[1:]
   358  		}
   359  		typeref += "{}"
   360  	}
   361  	var nameSuffix string
   362  	if view != "default" {
   363  		nameSuffix = codegen.Goify(view, true)
   364  	}
   365  	return map[string]interface{}{
   366  		"Name":    ok.Name + nameSuffix,
   367  		"GoType":  codegen.GoNativeType(pmt),
   368  		"TypeRef": typeref,
   369  	}
   370  }
   371  
   372  // funcMap creates the funcMap used to render the controller code.
   373  func funcMap(appPkg string, actionImpls map[string]string) template.FuncMap {
   374  	return template.FuncMap{
   375  		"tempvar":   tempvar,
   376  		"okResp":    okResp,
   377  		"targetPkg": func() string { return appPkg },
   378  		"actionBody": func(name string) string {
   379  			body, ok := actionImpls[name]
   380  			if !ok {
   381  				return defaultActionBody
   382  			}
   383  			return body
   384  		},
   385  		"printResp": func(name string) bool {
   386  			_, ok := actionImpls[name]
   387  			return !ok
   388  		},
   389  	}
   390  }
   391  
   392  var linePattern = regexp.MustCompile(`^\s*// ([^:]+): (\w+)_implement\s*$`)
   393  
   394  const defaultActionBody = `// Put your logic here`
   395  
   396  const ctrlT = `// {{ $ctrlName := printf "%s%s" (goify .Name true) "Controller" }}{{ $ctrlName }} implements the {{ .Name }} resource.
   397  type {{ $ctrlName }} struct {
   398  	*goa.Controller
   399  }
   400  
   401  // New{{ $ctrlName }} creates a {{ .Name }} controller.
   402  func New{{ $ctrlName }}(service *goa.Service) *{{ $ctrlName }} {
   403  	return &{{ $ctrlName }}{Controller: service.NewController("{{ $ctrlName }}")}
   404  }
   405  `
   406  
   407  const actionT = `
   408  {{- $ctrlName := printf "%s%s" (goify .Parent.Name true) "Controller" -}}
   409  {{- $actionDescr := printf "%s_%s" $ctrlName (goify .Name true) -}}
   410  // {{ goify .Name true }} runs the {{ .Name }} action.
   411  func (c *{{ $ctrlName }}) {{ goify .Name true }}(ctx *{{ targetPkg }}.{{ goify .Name true }}{{ goify .Parent.Name true }}Context) error {
   412  	// {{ $actionDescr }}: start_implement
   413  
   414  	{{ actionBody $actionDescr }}
   415  
   416  {{ if printResp $actionDescr }}
   417  {{ $ok := okResp . targetPkg }}{{ if $ok }} res := {{ $ok.TypeRef }}
   418  {{ end }} return {{ if $ok }}ctx.{{ $ok.Name }}(res){{ else }}nil{{ end }}
   419  {{ end }}	// {{ $actionDescr }}: end_implement
   420  }
   421  `
   422  
   423  const actionWST = `
   424  {{- $ctrlName := printf "%s%s" (goify .Parent.Name true) "Controller" -}}
   425  {{- $actionDescr := printf "%s_%s" $ctrlName (goify .Name true) -}}
   426  // {{ goify .Name true }} runs the {{ .Name }} action.
   427  func (c *{{ $ctrlName }}) {{ goify .Name true }}(ctx *{{ targetPkg }}.{{ goify .Name true }}{{ goify .Parent.Name true }}Context) error {
   428  	c.{{ goify .Name true }}WSHandler(ctx).ServeHTTP(ctx.ResponseWriter, ctx.Request)
   429  	return nil
   430  }
   431  
   432  // {{ goify .Name true }}WSHandler establishes a websocket connection to run the {{ .Name }} action.
   433  func (c *{{ $ctrlName }}) {{ goify .Name true }}WSHandler(ctx *{{ targetPkg }}.{{ goify .Name true }}{{ goify .Parent.Name true }}Context) websocket.Handler {
   434  	return func(ws *websocket.Conn) {
   435  		// {{ $actionDescr }}: start_implement
   436  
   437  		{{ actionBody $actionDescr }}
   438  {{ if printResp $actionDescr }}
   439  		ws.Write([]byte("{{ .Name }} {{ .Parent.Name }}"))
   440  		// Dummy echo websocket server
   441  		io.Copy(ws, ws)
   442  {{ end }}		// {{ $actionDescr }}: end_implement
   443  	}
   444  }`
   445  
   446  const mainT = `
   447  func main() {
   448  	// Create service
   449  	service := goa.New({{ printf "%q" .Name }})
   450  
   451  	// Mount middleware
   452  	service.Use(middleware.RequestID())
   453  	service.Use(middleware.LogRequest(true))
   454  	service.Use(middleware.ErrorHandler(service, true))
   455  	service.Use(middleware.Recover())
   456  {{ $api := .API }}
   457  {{ range $name, $res := $api.Resources }}{{ $name := goify $res.Name true }} // Mount "{{$res.Name}}" controller
   458  	{{ $tmp := tempvar }}{{ $tmp }} := New{{ $name }}Controller(service)
   459  	{{ targetPkg }}.Mount{{ $name }}Controller(service, {{ $tmp }})
   460  {{ end }}
   461  
   462  	// Start service
   463  	if err := service.ListenAndServe(":{{ getPort .API.Host }}"); err != nil {
   464  		service.LogError("startup", "err", err)
   465  	}
   466  }
   467  `