diff --git a/cmd/goa/gen.go b/cmd/goa/gen.go index 37c7efbbab..5791fb0074 100644 --- a/cmd/goa/gen.go +++ b/cmd/goa/gen.go @@ -30,6 +30,9 @@ type Generator struct { // Output is the absolute path to the output directory. Output string + // ServiceOutput is the absolute path to the example service output directory. + ServiceOutput string + // DesignVersion is the major component of the Goa version used by the design DSL. // DesignVersion is either 2 or 3. DesignVersion int @@ -45,7 +48,7 @@ type Generator struct { } // NewGenerator creates a Generator. -func NewGenerator(cmd, path, output string, debug bool) *Generator { +func NewGenerator(cmd, path, output, serviceOutput string, debug bool) *Generator { bin := "goa" if runtime.GOOS == "windows" { bin += ".exe" @@ -96,6 +99,7 @@ func NewGenerator(cmd, path, output string, debug bool) *Generator { Command: cmd, DesignPath: path, Output: output, + ServiceOutput: serviceOutput, DesignVersion: version, hasVendorDirectory: hasVendorDirectory, bin: bin, @@ -229,7 +233,7 @@ func (g *Generator) Run(debug bool) ([]string, error) { cmdl = fmt.Sprintf("$ %s%s", rawcmd, cmdl) } - args := []string{"--version=" + strconv.Itoa(g.DesignVersion), "--output=" + g.Output, "--cmd=" + cmdl, "--debug=" + strconv.FormatBool(debug)} + args := []string{"--version=" + strconv.Itoa(g.DesignVersion), "--output=" + g.Output, "--service-output=" + g.ServiceOutput, "--cmd=" + cmdl, "--debug=" + strconv.FormatBool(debug)} cmd := exec.Command(filepath.Join(g.tmpDir, g.bin), args...) out, err := cmd.CombinedOutput() if err != nil { @@ -307,10 +311,11 @@ func cleanupDirs(cmd, output string) []string { // mainT is the template for the generator main. const mainT = `func main() { var ( - out = flag.String("output", "", "") - version = flag.String("version", "", "") - cmdl = flag.String("cmd", "", "") - debug = flag.Bool("debug", false, "") + out = flag.String("output", "", "") + serviceOut = flag.String("service-output", "", "") + version = flag.String("version", "", "") + cmdl = flag.String("cmd", "", "") + debug = flag.Bool("debug", false, "") ver int ) { @@ -318,6 +323,9 @@ const mainT = `func main() { if *out == "" { fail("missing output flag") } + if *serviceOut == "" { + *serviceOut = *out + } if *version == "" { fail("missing version flag") } @@ -366,7 +374,7 @@ const mainT = `func main() { {{- end }} startGenerate := time.Now() - outputs, err := generator.Generate(*out, {{ printf "%q" .Command }}, *debug) + outputs, err := generator.Generate(*out, {{ printf "%q" .Command }}, *serviceOut, *debug) if err != nil { fail(err.Error()) } diff --git a/cmd/goa/main.go b/cmd/goa/main.go index 0129aaf337..319ed3eb6d 100644 --- a/cmd/goa/main.go +++ b/cmd/goa/main.go @@ -41,14 +41,16 @@ func main() { } var ( - output = "." - debug bool + output = "." + serviceOutput = "" + debug bool ) if len(os.Args) > offset+1 { var ( - fset = flag.NewFlagSet("default", flag.ExitOnError) - o = fset.String("o", "", "output `directory`") - out = fset.String("output", output, "output `directory`") + fset = flag.NewFlagSet("default", flag.ExitOnError) + o = fset.String("o", "", "output `directory`") + out = fset.String("output", output, "output `directory`") + serviceOut = fset.String("service-output", "", "service output `directory`") ) fset.BoolVar(&debug, "debug", false, "Print debug information") @@ -62,9 +64,16 @@ func main() { if output == "" { output = *out } + serviceOutput = *serviceOut + if serviceOutput == "" { + serviceOutput = output + } + } + if serviceOutput == "" { + serviceOutput = output } - if err := gen(cmd, path, output, debug); err != nil { + if err := gen(cmd, path, output, serviceOutput, debug); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } @@ -76,7 +85,7 @@ var ( gen = generate ) -func generate(cmd, path, output string, debug bool) error { +func generate(cmd, path, output, serviceOutput string, debug bool) error { var ( files []string err error @@ -99,7 +108,7 @@ func generate(cmd, path, output string, debug bool) error { } startNewGen = time.Now() - tmp = NewGenerator(cmd, path, output, debug) + tmp = NewGenerator(cmd, path, output, serviceOutput, debug) if debug { fmt.Fprintf(os.Stderr, "[TIMING] NewGenerator took %v\n", time.Since(startNewGen)) } @@ -146,7 +155,7 @@ Learn more at https://goa.design. Usage: goa gen PACKAGE [--output DIRECTORY] [--debug] - goa example PACKAGE [--output DIRECTORY] [--debug] + goa example PACKAGE [--output DIRECTORY] [--service-output DIRECTORY] [--debug] goa version Commands: @@ -165,6 +174,9 @@ Flags: -o, -output DIRECTORY output directory, defaults to the current working directory + -service-output DIRECTORY + service example output directory, defaults to the output directory + -debug Print debug information (mainly intended for Goa developers) diff --git a/cmd/goa/main_test.go b/cmd/goa/main_test.go index f9dad07c4f..3b369debc2 100644 --- a/cmd/goa/main_test.go +++ b/cmd/goa/main_test.go @@ -12,37 +12,44 @@ func TestCmdLine(t *testing.T) { testOutput = "testOutput" ) var ( - usageCalled bool - cmd string - path, output string - debug bool + usageCalled bool + cmd string + path, output string + serviceOutput string + debug bool ) usage = func() { usageCalled = true } - gen = func(c string, p, o string, d bool) error { cmd, path, output, debug = c, p, o, d; return nil } + gen = func(c, p, o, so string, d bool) error { + cmd, path, output, serviceOutput, debug = c, p, o, so, d + return nil + } defer func() { usage = help gen = generate }() cases := map[string]struct { - CmdLine string - ExpectedUsage bool - ExpectedCommand string - ExpectedPath string - ExpectedOutput string - ExpectedDebug bool + CmdLine string + ExpectedUsage bool + ExpectedCommand string + ExpectedPath string + ExpectedOutput string + ExpectedServiceOutput string + ExpectedDebug bool }{ - "gen": {"gen " + testPkg, false, "gen", testPkg, ".", false}, + "gen": {"gen " + testPkg, false, "gen", testPkg, ".", ".", false}, + "example default": {"example " + testPkg, false, "example", testPkg, ".", ".", false}, - "invalid": {"invalid " + testPkg, true, "", "", "", false}, - "empty": {"", true, "", "", "", false}, - "invalid gen": {"invalid gen" + testPkg, true, "", "", "", false}, + "invalid": {"invalid " + testPkg, true, "", "", "", "", false}, + "empty": {"", true, "", "", "", "", false}, + "invalid gen": {"invalid gen" + testPkg, true, "", "", "", "", false}, - "output": {"gen " + testPkg + " -output " + testOutput, false, "gen", testPkg, testOutput, false}, - "output short": {"gen " + testPkg + " -o " + testOutput, false, "gen", testPkg, testOutput, false}, + "output": {"gen " + testPkg + " -output " + testOutput, false, "gen", testPkg, testOutput, testOutput, false}, + "output short": {"gen " + testPkg + " -o " + testOutput, false, "gen", testPkg, testOutput, testOutput, false}, + "service-output": {"example " + testPkg + " -service-output " + testOutput, false, "example", testPkg, ".", testOutput, false}, - "debug": {"gen " + testPkg + " -debug", false, "gen", testPkg, ".", true}, + "debug": {"gen " + testPkg + " -debug", false, "gen", testPkg, ".", ".", true}, } for k, c := range cases { @@ -53,6 +60,7 @@ func TestCmdLine(t *testing.T) { cmd = "" path = "" output = "" + serviceOutput = "" debug = false } @@ -70,6 +78,9 @@ func TestCmdLine(t *testing.T) { if output != c.ExpectedOutput { t.Errorf("%s: Expected output to be %s but got %s", k, c.ExpectedOutput, output) } + if serviceOutput != c.ExpectedServiceOutput { + t.Errorf("%s: Expected service output to be %s but got %s", k, c.ExpectedServiceOutput, serviceOutput) + } if debug != c.ExpectedDebug { t.Errorf("%s: Expected debug to be %v but got %v", k, c.ExpectedDebug, debug) } diff --git a/codegen/file.go b/codegen/file.go index fc157cbcc0..5f5c7b82f2 100644 --- a/codegen/file.go +++ b/codegen/file.go @@ -30,6 +30,9 @@ type ( SectionTemplates []*SectionTemplate // Path returns the file path relative to the output directory. Path string + // OutputDir overrides the base directory used when rendering the file. + // When empty, Render uses the directory passed as argument. + OutputDir string // SkipExist indicates whether the file should be skipped if one // already exists at the given path. SkipExist bool @@ -70,7 +73,11 @@ func (f *File) Section(name string) []*SectionTemplate { // happens the smallest integer value greater than 1 to make it unique. Renders // returns the computed path. func (f *File) Render(dir string) (string, error) { - base, err := filepath.Abs(dir) + baseDir := dir + if f.OutputDir != "" { + baseDir = f.OutputDir + } + base, err := filepath.Abs(baseDir) if err != nil { return "", err } diff --git a/codegen/generator/example.go b/codegen/generator/example.go index 9e936565d2..abea00c065 100644 --- a/codegen/generator/example.go +++ b/codegen/generator/example.go @@ -13,7 +13,7 @@ import ( // Example iterates through the roots and returns files that implement an // example service, server, and client. -func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) { +func Example(genpkg string, roots []eval.Root, serviceOutput string) ([]*codegen.File, error) { var files []*codegen.File for _, root := range roots { r, ok := root.(*expr.RootExpr) @@ -25,7 +25,7 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) { services := service.NewServicesData(r) // example service implementation - if fs := service.ExampleServiceFiles(genpkg, r, services); len(fs) != 0 { + if fs := service.ExampleServiceFiles(genpkg, r, services, serviceOutput); len(fs) != 0 { files = append(files, fs...) } diff --git a/codegen/generator/generate.go b/codegen/generator/generate.go index 174a453d89..ca5841b58b 100644 --- a/codegen/generator/generate.go +++ b/codegen/generator/generate.go @@ -15,7 +15,7 @@ import ( ) // Generate runs the code generation algorithms. -func Generate(dir, cmd string, debug bool) (outputs []string, err1 error) { +func Generate(dir, cmd, serviceOutput string, debug bool) (outputs []string, err1 error) { startGenerate := time.Now() if debug { fmt.Fprintf(os.Stderr, "[TIMING] [generate] Starting generator.Generate()\n") @@ -119,7 +119,7 @@ func Generate(dir, cmd string, debug bool) (outputs []string, err1 error) { start := time.Now() for i, gen := range genfuncs { genStart := time.Now() - fs, err := gen(genpkg, roots) + fs, err := gen(genpkg, roots, serviceOutput) if err != nil { return nil, err } diff --git a/codegen/generator/generate_merge_test.go b/codegen/generator/generate_merge_test.go index e4659adf66..ab043f4e23 100644 --- a/codegen/generator/generate_merge_test.go +++ b/codegen/generator/generate_merge_test.go @@ -25,7 +25,7 @@ func TestGenerateMergesSamePathFiles(t *testing.T) { // second write would overwrite the first. Generators = func(cmd string) ([]Genfunc, error) { return []Genfunc{ - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { f := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "merge_test.go")} f.SectionTemplates = []*codegen.SectionTemplate{ codegen.Header("User types", "types", nil), @@ -36,7 +36,7 @@ func TestGenerateMergesSamePathFiles(t *testing.T) { } return []*codegen.File{f}, nil }, - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { f := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "merge_test.go")} f.SectionTemplates = []*codegen.SectionTemplate{ codegen.Header("User types", "types", nil), @@ -51,7 +51,7 @@ func TestGenerateMergesSamePathFiles(t *testing.T) { } dir := t.TempDir() - _, err := Generate(dir, "gen", false) + _, err := Generate(dir, "gen", dir, false) if err != nil { t.Fatalf("Generate failed: %v", err) } @@ -83,7 +83,7 @@ func TestGenerateParallelManyFiles(t *testing.T) { const numFiles = 20 Generators = func(cmd string) ([]Genfunc, error) { return []Genfunc{ - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { files := make([]*codegen.File, numFiles) for i := 0; i < numFiles; i++ { f := &codegen.File{ @@ -104,7 +104,7 @@ func TestGenerateParallelManyFiles(t *testing.T) { } dir := t.TempDir() - outputs, err := Generate(dir, "gen", false) + outputs, err := Generate(dir, "gen", dir, false) if err != nil { t.Fatalf("Generate failed: %v", err) } @@ -140,7 +140,7 @@ func TestGenerateParallelWithMerge(t *testing.T) { // This exercises both merging and parallel writing with NumCPU workers. Generators = func(cmd string) ([]Genfunc, error) { return []Genfunc{ - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { f1 := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "merged.go")} f1.SectionTemplates = []*codegen.SectionTemplate{ codegen.Header("Types", "types", nil), @@ -148,7 +148,7 @@ func TestGenerateParallelWithMerge(t *testing.T) { } return []*codegen.File{f1}, nil }, - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { f2 := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "merged.go")} f2.SectionTemplates = []*codegen.SectionTemplate{ codegen.Header("Types", "types", nil), @@ -156,7 +156,7 @@ func TestGenerateParallelWithMerge(t *testing.T) { } return []*codegen.File{f2}, nil }, - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { f3 := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "separate.go")} f3.SectionTemplates = []*codegen.SectionTemplate{ codegen.Header("Types", "types", nil), @@ -168,7 +168,7 @@ func TestGenerateParallelWithMerge(t *testing.T) { } dir := t.TempDir() - outputs, err := Generate(dir, "gen", false) + outputs, err := Generate(dir, "gen", dir, false) if err != nil { t.Fatalf("Generate failed: %v", err) } @@ -214,7 +214,7 @@ func TestGenerateParallelErrorHandling(t *testing.T) { // Worker pool should capture first error but continue processing other files. Generators = func(cmd string) ([]Genfunc, error) { return []Genfunc{ - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { files := make([]*codegen.File, 5) for i := 0; i < 5; i++ { f := &codegen.File{ @@ -239,7 +239,7 @@ func TestGenerateParallelErrorHandling(t *testing.T) { } dir := t.TempDir() - _, err := Generate(dir, "gen", false) + _, err := Generate(dir, "gen", dir, false) if err == nil { t.Fatal("expected error from parallel generation, got nil") } @@ -256,7 +256,7 @@ func TestGenerateParallelSingleFile(t *testing.T) { Generators = func(cmd string) ([]Genfunc, error) { return []Genfunc{ - func(genpkg string, roots []eval.Root) ([]*codegen.File, error) { + func(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { f := &codegen.File{Path: filepath.Join(codegen.Gendir, "types", "single.go")} f.SectionTemplates = []*codegen.SectionTemplate{ codegen.Header("Types", "types", nil), @@ -268,7 +268,7 @@ func TestGenerateParallelSingleFile(t *testing.T) { } dir := t.TempDir() - outputs, err := Generate(dir, "gen", false) + outputs, err := Generate(dir, "gen", dir, false) if err != nil { t.Fatalf("Generate failed: %v", err) } diff --git a/codegen/generator/generate_union_merge_integration_test.go b/codegen/generator/generate_union_merge_integration_test.go index b4bfc7dbcf..19da49951d 100644 --- a/codegen/generator/generate_union_merge_integration_test.go +++ b/codegen/generator/generate_union_merge_integration_test.go @@ -55,7 +55,7 @@ func TestGenerateUnionUserTypeSamePathMerged(t *testing.T) { _ = cg.RunDSL(t, dsl) dir := t.TempDir() - if _, err := Generate(dir, "gen", false); err != nil { + if _, err := Generate(dir, "gen", dir, false); err != nil { t.Fatalf("Generate failed: %v", err) } diff --git a/codegen/generator/generators.go b/codegen/generator/generators.go index e949d35391..76f211f480 100644 --- a/codegen/generator/generators.go +++ b/codegen/generator/generators.go @@ -8,7 +8,7 @@ import ( ) // Genfunc is the type of the functions invoked to generate code. -type Genfunc func(genpkg string, roots []eval.Root) ([]*codegen.File, error) +type Genfunc func(genpkg string, roots []eval.Root, serviceOutput string) ([]*codegen.File, error) // Generators returns the qualified paths (including the package name) to the // code generator functions for the given command, an error if the command is diff --git a/codegen/generator/openapi.go b/codegen/generator/openapi.go index 0381d2231a..a45b9a1c01 100644 --- a/codegen/generator/openapi.go +++ b/codegen/generator/openapi.go @@ -10,7 +10,7 @@ import ( // OpenAPI iterates through the roots and returns the files needed to render // the service OpenAPI spec. It produces OpenAPI specifications only if the // roots define a HTTP service. -func OpenAPI(_ string, roots []eval.Root) ([]*codegen.File, error) { +func OpenAPI(_ string, roots []eval.Root, _ string) ([]*codegen.File, error) { for _, root := range roots { if r, ok := root.(*expr.RootExpr); ok { return httpcodegen.OpenAPIFiles(r) diff --git a/codegen/generator/service.go b/codegen/generator/service.go index 3eeff7d670..f6454ac6d3 100644 --- a/codegen/generator/service.go +++ b/codegen/generator/service.go @@ -10,7 +10,7 @@ import ( // Service iterates through the roots and returns the files needed to render // the service code. It returns an error if the roots slice does not include // a goa design. -func Service(genpkg string, roots []eval.Root) ([]*codegen.File, error) { +func Service(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { var files []*codegen.File var userTypePkgs = make(map[string][]string) for _, root := range roots { diff --git a/codegen/generator/transport.go b/codegen/generator/transport.go index 9ed278de1d..0a847845a0 100644 --- a/codegen/generator/transport.go +++ b/codegen/generator/transport.go @@ -12,7 +12,7 @@ import ( // Transport iterates through the roots and returns the files needed to render // the transport code. -func Transport(genpkg string, roots []eval.Root) ([]*codegen.File, error) { +func Transport(genpkg string, roots []eval.Root, _ string) ([]*codegen.File, error) { var files []*codegen.File for _, root := range roots { r, ok := root.(*expr.RootExpr) diff --git a/codegen/service/example_svc.go b/codegen/service/example_svc.go index f92f822311..f06a8c873d 100644 --- a/codegen/service/example_svc.go +++ b/codegen/service/example_svc.go @@ -3,6 +3,7 @@ package service import ( "os" "path" + "path/filepath" "strings" "goa.design/goa/v3/codegen" @@ -35,7 +36,7 @@ type ( // ExampleServiceFiles returns a basic service implementation for every // service expression. -func ExampleServiceFiles(genpkg string, root *expr.RootExpr, services *ServicesData) []*codegen.File { +func ExampleServiceFiles(genpkg string, root *expr.RootExpr, services *ServicesData, serviceOutput string) []*codegen.File { // determine the unique API package name different from the service names scope := codegen.NewNameScope() for _, svc := range root.Services { @@ -49,7 +50,7 @@ func ExampleServiceFiles(genpkg string, root *expr.RootExpr, services *ServicesD var fw []*codegen.File for _, svc := range root.Services { - if f := exampleServiceFile(genpkg, root, svc, services, apipkg); f != nil { + if f := exampleServiceFile(genpkg, root, svc, services, apipkg, serviceOutput); f != nil { fw = append(fw, f) } } @@ -57,10 +58,10 @@ func ExampleServiceFiles(genpkg string, root *expr.RootExpr, services *ServicesD } // exampleServiceFile returns a basic implementation of the given service. -func exampleServiceFile(genpkg string, _ *expr.RootExpr, svc *expr.ServiceExpr, services *ServicesData, apipkg string) *codegen.File { +func exampleServiceFile(genpkg string, _ *expr.RootExpr, svc *expr.ServiceExpr, services *ServicesData, apipkg, serviceOutput string) *codegen.File { data := services.Get(svc.Name) svcName := data.PathName - fpath := svcName + ".go" + fpath := filepath.Join(serviceOutput, svcName+".go") if _, err := os.Stat(fpath); !os.IsNotExist(err) { return nil // file already exists, skip it. } @@ -106,7 +107,8 @@ func exampleServiceFile(genpkg string, _ *expr.RootExpr, svc *expr.ServiceExpr, } return &codegen.File{ - Path: fpath, + Path: svcName + ".go", + OutputDir: serviceOutput, SectionTemplates: sections, SkipExist: true, } diff --git a/codegen/service/example_svc_test.go b/codegen/service/example_svc_test.go index 0de9c87dad..792cd4d4b8 100644 --- a/codegen/service/example_svc_test.go +++ b/codegen/service/example_svc_test.go @@ -2,6 +2,8 @@ package service import ( "bytes" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -34,7 +36,7 @@ func TestExampleServiceFiles(t *testing.T) { root := codegen.RunDSL(t, c.DSL) services := NewServicesData(root) require.Len(t, root.Services, 3) - fs := ExampleServiceFiles("", root, services) + fs := ExampleServiceFiles("", root, services, "") require.Len(t, fs, 3) for _, f := range fs { require.Greater(t, len(f.SectionTemplates), 0) @@ -48,4 +50,21 @@ func TestExampleServiceFiles(t *testing.T) { }) } }) + + t.Run("service output dir", func(t *testing.T) { + root := codegen.RunDSL(t, testdata.SingleMethodDSL) + services := NewServicesData(root) + serviceOutput := filepath.Join(t.TempDir(), "svc") + fs := ExampleServiceFiles("", root, services, serviceOutput) + require.Len(t, fs, 1) + renderDir := t.TempDir() + for _, f := range fs { + _, err := f.Render(renderDir) + require.NoError(t, err) + } + _, err := os.Stat(filepath.Join(serviceOutput, fs[0].Path)) + require.NoError(t, err) + _, err = os.Stat(filepath.Join(renderDir, fs[0].Path)) + require.Error(t, err) + }) }