Repository: goadesign/goa (go)

Issue:
v3: Package name is not unified if there is the same service name as API name
If there is a service name same as API name in design, the package name of go files generated by gen example is not unified.

## Example design

```go

    package design
    
    import (
       . "goa.design/goa/v3/dsl"
    )
    
    var _ = API("calc", func() {
       Title("Calculator Service")
       Description("Service for adding numbers, a Goa teaser")
       Server("calc", func() {
          Host("localhost", func() {
             URI("http://localhost:8000")
          })
       })
    })
    
    var _ = Service("calc", func() {
       Description("The calc service performs operations on numbers.")
    
       Method("add", func() {
          Payload(func() {
             Field(1, "a", Int, "Left operand")
             Field(2, "b", Int, "Right operand")
             Required("a", "b")
          })
    
          Result(Int)
    
          HTTP(func() {
             GET("/add/{a}/{b}")
          })
       })
    })
    
    var _ = Service("swagger", func() {
       Files("/openapi.json", "./gen/http/openapi.json")
    })
    
    var _ = Service("other service", func() {
       Description("The calc service performs operations on numbers.")
    
       Method("foo", func() {
          Payload(func() {
             Field(1, "a", Int)
          })
          Result(Int)
          HTTP(func() {
             GET("/foo/{a}")
          })
       })
    })
```

## Generated code

```shellsession

$ goa gen calc/design
...snip...
$ goa example calc/design
calc.go
cmd/calc-cli/http.go
cmd/calc-cli/main.go
cmd/calc/http.go
cmd/calc/main.go
other_service.go
swagger.go
$ ls *.go|xargs head -1
==> calc.go <==
package calcapi                // ←★ Only this package name is escaped.

==> other_service.go <==
package calc

==> swagger.go <==
package calc
```

## Bug?

`exampleServiceFile()`  avoids generating package names identical to API names. So in `calc` service, the package name `calc` is escaped and `calcapi` is used. But other services do not know that the package name has been escaped.

Original ExampleServiceFiles:
```go

// ExampleServiceFiles returns a basic service implementation for every
// service expression.
func ExampleServiceFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
   var fw []*codegen.File
   for _, svc := range root.Services {
      if f := exampleServiceFile(genpkg, root, svc); f != nil {
         fw = append(fw, f)
      }
   }
   return fw
}
```

So I think it is necessary to decide the package name before calculating the `exampleServiceFile` and pass it to the process. 

For example, 
```go

func ExampleServiceFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
   scope := codegen.NewNameScope()
   for _, svc := range root.Services {
      data := Services.Get(svc.Name)
      fmt.Println("data.PkgName", data.PkgName)
      scope.Unique(data.PkgName)
   }
   apiPkg := scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
   root.API.Meta = map[string][]string{}
   root.API.Meta["Pkg"] = []string{apiPkg} // ← ★ unique service package name

   var fw []*codegen.File
   for _, svc := range root.Services {
      if f := exampleServiceFile(genpkg, root, svc); f != nil {
         fw = append(fw, f)
      }
   }
   return fw
}
```
and

```go
// exampleServiceFile returns a basic implementation of the given service.
func exampleServiceFile(genpkg string, root *expr.RootExpr, svc *expr.ServiceExpr) *codegen.File {
   data := Services.Get(svc.Name)
   svcName := codegen.SnakeCase(data.VarName)
   fpath := svcName + ".go"
   if _, err := os.Stat(fpath); !os.IsNotExist(err) {
      return nil // file already exists, skip it.
   }
   scope := codegen.NewNameScope()
   specs := []*codegen.ImportSpec{
      {Path: "context"},
      {Path: "log"},
      {Path: path.Join(genpkg, codegen.SnakeCase(svcName)), Name: scope.Unique(data.PkgName)},
   }
   //apiPkg := scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
   apiPkg := root.API.Meta["Pkg"][0]   // ← ★ use it
```



Pull request:
V3: Fix the case where non-unified package names are generated
Fix #2153 

### Example design

```go
package design

import (
	. "goa.design/goa/v3/dsl"
)

var _ = API("calc", func() {
	Title("Calculator Service")
	Description("Service for adding numbers, a Goa teaser")
	Server("calc", func() {
		Host("localhost", func() {
			URI("http://localhost:8000")
			URI("grpc://localhost:8080")
		})
	})
})

var _ = Service("calc", func() {
	Description("The calc service performs operations on numbers.")

	Method("add", func() {
		Payload(func() {
			Field(1, "a", Int, "Left operand")
			Field(2, "b", Int, "Right operand")
			Required("a", "b")
		})

		Result(Int)

		HTTP(func() {
			GET("/add/{a}/{b}")
		})

		GRPC(func() {
		})
	})

	Files("/openapi.json", "./gen/http/openapi.json")
})

var _ = Service("calcapi", func() {
	Description("The calc service performs operations on numbers.")

	Method("add", func() {
		Payload(func() {
			Field(1, "a", Int, "Left operand")
			Field(2, "b", Int, "Right operand")
			Required("a", "b")
		})

		Result(Int)

		HTTP(func() {
			GET("/add/{a}/{b}")
		})

		GRPC(func() {
		})
	})

	Files("/openapi.json", "./gen/http/openapi.json")
})

var _ = Service("calcapi1", func() {
	Description("The calc service performs operations on numbers.")

	Method("add", func() {
		Payload(func() {
			Field(1, "a", Int, "Left operand")
			Field(2, "b", Int, "Right operand")
			Required("a", "b")
		})

		Result(Int)

		HTTP(func() {
			GET("/add/{a}/{b}")
		})

		GRPC(func() {
		})
	})

	Files("/openapi.json", "./gen/http/openapi.json")
})

```
### goa gen & goa example

```shellsession
$ goa gen calc/design
gen/calc/client.go
gen/calc/endpoints.go
gen/calc/service.go
gen/calcapi/client.go
gen/calcapi/endpoints.go
gen/calcapi/service.go
gen/calcapi1/client.go
gen/calcapi1/endpoints.go
gen/calcapi1/service.go
gen/grpc/calc/client/cli.go
gen/grpc/calc/client/client.go
gen/grpc/calc/client/encode_decode.go
gen/grpc/calc/client/types.go
gen/grpc/calc/pb/calc.proto
gen/grpc/calc/server/encode_decode.go
gen/grpc/calc/server/server.go
gen/grpc/calc/server/types.go
gen/grpc/calcapi/client/cli.go
gen/grpc/calcapi/client/client.go
gen/grpc/calcapi/client/encode_decode.go
gen/grpc/calcapi/client/types.go
gen/grpc/calcapi/pb/calcapi.proto
gen/grpc/calcapi/server/encode_decode.go
gen/grpc/calcapi/server/server.go
gen/grpc/calcapi/server/types.go
gen/grpc/calcapi1/client/cli.go
gen/grpc/calcapi1/client/client.go
gen/grpc/calcapi1/client/encode_decode.go
gen/grpc/calcapi1/client/types.go
gen/grpc/calcapi1/pb/calcapi1.proto
gen/grpc/calcapi1/server/encode_decode.go
gen/grpc/calcapi1/server/server.go
gen/grpc/calcapi1/server/types.go
gen/grpc/cli/calc/cli.go
gen/http/calc/client/cli.go
gen/http/calc/client/client.go
gen/http/calc/client/encode_decode.go
gen/http/calc/client/paths.go
gen/http/calc/client/types.go
gen/http/calc/server/encode_decode.go
gen/http/calc/server/paths.go
gen/http/calc/server/server.go
gen/http/calc/server/types.go
gen/http/calcapi/client/cli.go
gen/http/calcapi/client/client.go
gen/http/calcapi/client/encode_decode.go
gen/http/calcapi/client/paths.go
gen/http/calcapi/client/types.go
gen/http/calcapi/server/encode_decode.go
gen/http/calcapi/server/paths.go
gen/http/calcapi/server/server.go
gen/http/calcapi/server/types.go
gen/http/calcapi1/client/cli.go
gen/http/calcapi1/client/client.go
gen/http/calcapi1/client/encode_decode.go
gen/http/calcapi1/client/paths.go
gen/http/calcapi1/client/types.go
gen/http/calcapi1/server/encode_decode.go
gen/http/calcapi1/server/paths.go
gen/http/calcapi1/server/server.go
gen/http/calcapi1/server/types.go
gen/http/cli/calc/cli.go
gen/http/openapi.json
gen/http/openapi.yaml

$ gen example calc/design
calc.go
calcapi.go
calcapi1.go
cmd/calc-cli/grpc.go
cmd/calc-cli/http.go
cmd/calc-cli/main.go
cmd/calc/grpc.go
cmd/calc/http.go
cmd/calc/main.go
```

### Example service package name

```shellsession
$ ls *.go|xargs head -1
==> calc.go <==
package calcapi2

==> calcapi.go <==
package calcapi2

==> calcapi1.go <==
package calcapi2
```



Patch:
diff --git a/codegen/service/example_svc.go b/codegen/service/example_svc.go
index 5744263b..6cc64dd0 100644
--- a/codegen/service/example_svc.go
+++ b/codegen/service/example_svc.go
@@ -36,9 +36,21 @@ type (
 // ExampleServiceFiles returns a basic service implementation for every
 // service expression.
 func ExampleServiceFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
+
+	// determine the unique API package name different from the service names
+	scope := codegen.NewNameScope()
+	for _, svc := range root.Services {
+		svc := Services.Get(svc.Name)
+		if svc == nil {
+			panic("unknown service, " + svc.Name) // bug
+		}
+		scope.Unique(svc.PkgName)
+	}
+	apipkg := scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
+
 	var fw []*codegen.File
 	for _, svc := range root.Services {
-		if f := exampleServiceFile(genpkg, root, svc); f != nil {
+		if f := exampleServiceFile(genpkg, root, svc, apipkg); f != nil {
 			fw = append(fw, f)
 		}
 	}
@@ -46,22 +58,20 @@ func ExampleServiceFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
 }
 
 // exampleServiceFile returns a basic implementation of the given service.
-func exampleServiceFile(genpkg string, root *expr.RootExpr, svc *expr.ServiceExpr) *codegen.File {
+func exampleServiceFile(genpkg string, root *expr.RootExpr, svc *expr.ServiceExpr, apipkg string) *codegen.File {
 	data := Services.Get(svc.Name)
 	svcName := codegen.SnakeCase(data.VarName)
 	fpath := svcName + ".go"
 	if _, err := os.Stat(fpath); !os.IsNotExist(err) {
 		return nil // file already exists, skip it.
 	}
-	scope := codegen.NewNameScope()
 	specs := []*codegen.ImportSpec{
 		{Path: "context"},
 		{Path: "log"},
-		{Path: path.Join(genpkg, codegen.SnakeCase(svcName)), Name: scope.Unique(data.PkgName)},
+		{Path: path.Join(genpkg, codegen.SnakeCase(svcName)), Name: data.PkgName},
 	}
-	apiPkg := scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
 	sections := []*codegen.SectionTemplate{
-		codegen.Header("", apiPkg, specs),
+		codegen.Header("", apipkg, specs),
 		{Name: "basic-service-struct", Source: svcStructT, Data: data},
 		{Name: "basic-service-init", Source: svcInitT, Data: data},
 	}


Test patch:
diff --git a/codegen/service/example_svc_test.go b/codegen/service/example_svc_test.go
new file mode 100644
index 00000000..d048cfdf
--- /dev/null
+++ b/codegen/service/example_svc_test.go
@@ -0,0 +1,59 @@
+package service
+
+import (
+	"bytes"
+	"reflect"
+	"testing"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/codegen/service/testdata"
+	"goa.design/goa/v3/expr"
+)
+
+func TestExampleServiceFiles(t *testing.T) {
+	t.Run("package name check", func(t *testing.T) {
+		cases := []struct {
+			Name     string
+			DSL      func()
+			Expected string
+		}{
+			{
+				Name:     "conflict with API name and service names",
+				DSL:      testdata.ConflictWithAPINameAndServiceNameDSL,
+				Expected: "package alohaapi2",
+			},
+			{
+				Name:     "conflict with goified API name and goified service names",
+				DSL:      testdata.ConflictWithGoifiedAPINameAndServiceNamesDSL,
+				Expected: "package goodbyapi2",
+			},
+		}
+		for _, c := range cases {
+			t.Run(c.Name, func(t *testing.T) {
+				codegen.RunDSL(t, c.DSL)
+				expr.Root.GeneratedTypes = &expr.GeneratedRoot{}
+				if len(expr.Root.Services) != 3 {
+					t.Fatalf("got %d services, expected 3", len(expr.Root.Services))
+				}
+				fs := ExampleServiceFiles("", expr.Root)
+				if len(fs) != 3 {
+					t.Fatalf("got %d example file services, expected 3", len(fs))
+				}
+				for _, f := range fs {
+					if len(f.SectionTemplates) == 0 {
+						t.Fatalf("got empty templates, expected not empty")
+					}
+					var b bytes.Buffer
+					if err := f.SectionTemplates[0].Write(&b); err != nil {
+						t.Fatal(err)
+					}
+					if line, err := b.ReadBytes('\n'); err != nil {
+						t.Fatal(err)
+					} else if got := string(bytes.TrimRight(line, "\n")); !reflect.DeepEqual(got, c.Expected) {
+						t.Fatalf("got %s, expected %s", got, c.Expected)
+					}
+				}
+			})
+		}
+	})
+}
diff --git a/codegen/service/testdata/example_svc_dsls.go b/codegen/service/testdata/example_svc_dsls.go
new file mode 100644
index 00000000..87cfff11
--- /dev/null
+++ b/codegen/service/testdata/example_svc_dsls.go
@@ -0,0 +1,23 @@
+package testdata
+
+import (
+	. "goa.design/goa/v3/dsl"
+)
+
+var ConflictWithAPINameAndServiceNameDSL = func() {
+	var _ = API("aloha", func() {
+		Title("conflict with API name and service names")
+	})
+	var _ = Service("aloha", func() {})     // same as API name
+	var _ = Service("alohaapi", func() {})  // API name + 'api' suffix
+	var _ = Service("alohaapi1", func() {}) // API name + 'api' suffix + sequential no.
+}
+
+var ConflictWithGoifiedAPINameAndServiceNamesDSL = func() {
+	var _ = API("good-by", func() {
+		Title("conflict with goified API name and goified service names")
+	})
+	var _ = Service("good-by-", func() {})      // Goify name is same as API name
+	var _ = Service("good-by-api", func() {})   // API name + 'api' suffix
+	var _ = Service("good-by-api-1", func() {}) // API name + 'api' suffix + sequential no.
+}