Repository: goadesign/goa
Pull request #3203 (2022-11-26 08:34:23)

OpenAPI v3 type names refer to openapi:typename in meta
Fixes: #3169 #3202

najeira: Thank you for your review. I will add the comment.

This fix works when defining Meta to Payload or Result, but not works when defining Meta to Type.

I will try to fix that.
najeira: works:

```go
Payload(func() {
	Meta("openapi:typename", "Bar")
	Attribute("name", String)
})
```

not works:

```go
var Foo = Type("Foo", func() {
	Meta("openapi:typename", "Bar")
	Attribute("name", String)
})
Payload(Foo)
```

Please let me know if you have any ideas or how to fix it.
najeira: I force pushed another implementaion.

This supports to the definition of Meta in Type and ResultType.
raphael: Thank you for the great contribution!

Patch:
diff --git a/dsl/meta.go b/dsl/meta.go
index f94c51e3..bc487b48 100644
--- a/dsl/meta.go
+++ b/dsl/meta.go
@@ -207,6 +207,14 @@ import (
 //	var _ = API("MyAPI", func() {
 //	    Meta("openapi:extension:x-api", `{"foo":"bar"}`)
 //	})
+//
+// - "openapi:typename" overrides the name of the type generated in the OpenAPI specification.
+// Applicable to types (including embedded Payload and Result definitions).
+//
+//	var Foo = Type("Foo", func() {
+//	    Attribute("name", String)
+//	    Meta("openapi:typename", "Bar")
+//	})
 func Meta(name string, value ...string) {
 	appendMeta := func(meta expr.MetaExpr, name string, value ...string) expr.MetaExpr {
 		if meta == nil {
diff --git a/http/codegen/openapi/v3/types.go b/http/codegen/openapi/v3/types.go
index f12e5d3b..5e5e9d0a 100644
--- a/http/codegen/openapi/v3/types.go
+++ b/http/codegen/openapi/v3/types.go
@@ -33,7 +33,7 @@ type (
 		// type schemas indexed by ref
 		schemas map[string]*openapi.Schema
 		// type names indexed by hashes
-		hashes map[uint64]string
+		hashes map[uint64][]string
 		rand   *expr.ExampleGenerator
 	}
 )
@@ -42,7 +42,7 @@ type (
 func newSchemafier(rand *expr.ExampleGenerator) *schemafier {
 	return &schemafier{
 		schemas: make(map[string]*openapi.Schema),
-		hashes:  make(map[uint64]string),
+		hashes:  make(map[uint64][]string),
 		rand:    rand,
 	}
 }
@@ -62,6 +62,21 @@ func newSchemafier(rand *expr.ExampleGenerator) *schemafier {
 func buildBodyTypes(api *expr.APIExpr) (map[string]map[string]*EndpointBodies, map[string]*openapi.Schema) {
 	bodies := make(map[string]map[string]*EndpointBodies)
 	sf := newSchemafier(api.ExampleGenerator)
+
+	// Generates the types referenced from the endpoints.
+	for _, t := range expr.Root.Types {
+		if !mustGenerateType(t.Attribute().Meta) {
+			continue
+		}
+		sf.schemafy(&expr.AttributeExpr{Type: t})
+	}
+	for _, t := range expr.Root.ResultTypes {
+		if !mustGenerateType(t.Attribute().Meta) {
+			continue
+		}
+		sf.schemafy(&expr.AttributeExpr{Type: t})
+	}
+
 	for _, s := range api.HTTP.Services {
 		if !mustGenerate(s.Meta) || !mustGenerate(s.ServiceExpr.Meta) {
 			continue
@@ -130,20 +145,6 @@ func buildBodyTypes(api *expr.APIExpr) (map[string]map[string]*EndpointBodies, m
 		}
 		bodies[s.Name()] = sbodies
 	}
-	for _, t := range expr.Root.Types {
-		_, ok := t.Attribute().Meta["type:generate:force"]
-		if !ok {
-			continue
-		}
-		sf.schemafy(&expr.AttributeExpr{Type: t})
-	}
-	for _, t := range expr.Root.ResultTypes {
-		_, ok := t.Attribute().Meta["type:generate:force"]
-		if !ok {
-			continue
-		}
-		sf.schemafy(&expr.AttributeExpr{Type: t})
-	}
 	return bodies, sf.schemas
 }
 
@@ -216,19 +217,36 @@ func (sf *schemafier) schemafy(attr *expr.AttributeExpr, noref ...bool) *openapi
 	case expr.UserType:
 		if !expr.IsAlias(t) {
 			h := sf.hashAttribute(attr, fnv.New64())
-			ref, ok := sf.hashes[h]
+
+			var metaName string
+			if n, ok := t.Attribute().Meta["openapi:typename"]; ok {
+				metaName = codegen.Goify(n[0], true)
+			}
+			metaRef := toRef(metaName)
+
+			// If it is named, it refers to the same structure and name.
+			// If it is not named, it refers to the same structure.
+			refs, ok := sf.hashes[h]
 			if len(noref) == 0 && ok {
-				s.Ref = ref
-			} else {
-				name := t.Name()
-				if n, ok := t.Attribute().Meta["name:original"]; ok {
-					name = n[0]
+				for _, ref := range refs {
+					if ref == metaRef || metaName == "" {
+						s.Ref = ref
+						return s
+					}
 				}
-				typeName := sf.uniquify(codegen.Goify(name, true))
-				s.Ref = toRef(typeName)
-				sf.hashes[h] = s.Ref
-				sf.schemas[typeName] = sf.schemafy(t.Attribute(), true)
 			}
+
+			// There is no type to refer to, generate a new one.
+			name := t.Name()
+			if metaName != "" {
+				name = metaName
+			} else if n, ok := t.Attribute().Meta["name:original"]; ok {
+				name = n[0]
+			}
+			typeName := sf.uniquify(codegen.Goify(name, true))
+			s.Ref = toRef(typeName)
+			sf.hashes[h] = append(sf.hashes[h], s.Ref)
+			sf.schemas[typeName] = sf.schemafy(t.Attribute(), true)
 			return s // All other schema properties are set in the reference
 		}
 		// Alias primitive type
@@ -457,3 +475,15 @@ func orderedHash(a, b uint64, h hash.Hash64) uint64 {
 	}
 	return h.Sum64()
 }
+
+func mustGenerateType(meta expr.MetaExpr) bool {
+	if _, ok := meta["type:generate:force"]; ok {
+		return true
+	}
+	if n, ok := meta.Last("openapi:typename"); ok {
+		if n != "" {
+			return true
+		}
+	}
+	return false
+}


Test patch:
diff --git a/http/codegen/openapi/v3/files_test.go b/http/codegen/openapi/v3/files_test.go
index a9e0fcfd..1f960fbb 100644
--- a/http/codegen/openapi/v3/files_test.go
+++ b/http/codegen/openapi/v3/files_test.go
@@ -44,6 +44,7 @@ func TestFiles(t *testing.T) {
 		{"path-with-wildcards", testdata.PathWithWildcardDSL},
 		{"with-tags", testdata.WithTagsDSL},
 		{"with-tags-swagger", testdata.WithTagsSwaggerDSL},
+		{"typename", testdata.TypenameDSL},
 		// TestEndpoints
 		{"endpoint", testdata.ExtensionDSL},
 		{"endpoint-swagger", testdata.ExtensionSwaggerDSL},
diff --git a/http/codegen/openapi/v3/testdata/golden/typename_file0.golden b/http/codegen/openapi/v3/testdata/golden/typename_file0.golden
new file mode 100644
index 00000000..bb6d972a
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/typename_file0.golden
@@ -0,0 +1,1 @@
+{"openapi":"3.0.3","info":{"title":"Goa API","version":"1.0"},"servers":[{"url":"https://goa.design"}],"paths":{"/bar":{"post":{"tags":["testService"],"summary":"bar testService","operationId":"testService#bar","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BarPayload"},"example":{"value":""}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BarResult"},"example":{"value":""}}}}}}},"/baz":{"post":{"tags":["testService"],"summary":"baz testService","operationId":"testService#baz","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BazPayload"},"example":{"value":""}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BazResult"},"example":{"value":""}}}}}}},"/foo":{"post":{"tags":["testService"],"summary":"foo testService","operationId":"testService#foo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FooPayload"},"example":{"value":""}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FooResult"},"example":{"value":""}}}}}}}},"components":{"schemas":{"BarPayload":{"type":"object","properties":{"value":{"type":"string","example":""}},"example":{"value":""}},"BarResult":{"type":"object","properties":{"value":{"type":"string","example":""}},"example":{"value":""}},"BazPayload":{"type":"object","properties":{"value":{"type":"string","example":""}},"example":{"value":""}},"BazResult":{"type":"object","properties":{"value":{"type":"string","example":""}},"example":{"value":""}},"FooPayload":{"type":"object","properties":{"value":{"type":"string","example":""}},"example":{"value":""}},"FooResult":{"type":"object","properties":{"value":{"type":"string","example":""}},"example":{"value":""}}}},"tags":[{"name":"testService"}]}
\ No newline at end of file
diff --git a/http/codegen/openapi/v3/testdata/golden/typename_file1.golden b/http/codegen/openapi/v3/testdata/golden/typename_file1.golden
new file mode 100644
index 00000000..e3166487
--- /dev/null
+++ b/http/codegen/openapi/v3/testdata/golden/typename_file1.golden
@@ -0,0 +1,128 @@
+openapi: 3.0.3
+info:
+    title: Goa API
+    version: "1.0"
+servers:
+    - url: https://goa.design
+paths:
+    /bar:
+        post:
+            tags:
+                - testService
+            summary: bar testService
+            operationId: testService#bar
+            requestBody:
+                required: true
+                content:
+                    application/json:
+                        schema:
+                            $ref: '#/components/schemas/BarPayload'
+                        example:
+                            value: ""
+            responses:
+                "200":
+                    description: OK response.
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/BarResult'
+                            example:
+                                value: ""
+    /baz:
+        post:
+            tags:
+                - testService
+            summary: baz testService
+            operationId: testService#baz
+            requestBody:
+                required: true
+                content:
+                    application/json:
+                        schema:
+                            $ref: '#/components/schemas/BazPayload'
+                        example:
+                            value: ""
+            responses:
+                "200":
+                    description: OK response.
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/BazResult'
+                            example:
+                                value: ""
+    /foo:
+        post:
+            tags:
+                - testService
+            summary: foo testService
+            operationId: testService#foo
+            requestBody:
+                required: true
+                content:
+                    application/json:
+                        schema:
+                            $ref: '#/components/schemas/FooPayload'
+                        example:
+                            value: ""
+            responses:
+                "200":
+                    description: OK response.
+                    content:
+                        application/json:
+                            schema:
+                                $ref: '#/components/schemas/FooResult'
+                            example:
+                                value: ""
+components:
+    schemas:
+        BarPayload:
+            type: object
+            properties:
+                value:
+                    type: string
+                    example: ""
+            example:
+                value: ""
+        BarResult:
+            type: object
+            properties:
+                value:
+                    type: string
+                    example: ""
+            example:
+                value: ""
+        BazPayload:
+            type: object
+            properties:
+                value:
+                    type: string
+                    example: ""
+            example:
+                value: ""
+        BazResult:
+            type: object
+            properties:
+                value:
+                    type: string
+                    example: ""
+            example:
+                value: ""
+        FooPayload:
+            type: object
+            properties:
+                value:
+                    type: string
+                    example: ""
+            example:
+                value: ""
+        FooResult:
+            type: object
+            properties:
+                value:
+                    type: string
+                    example: ""
+            example:
+                value: ""
+tags:
+    - name: testService
diff --git a/http/codegen/testdata/openapi_dsls.go b/http/codegen/testdata/openapi_dsls.go
index 340e91b3..cc13088a 100644
--- a/http/codegen/testdata/openapi_dsls.go
+++ b/http/codegen/testdata/openapi_dsls.go
@@ -622,3 +622,66 @@ var WithTagsSwaggerDSL = func() {
 		})
 	})
 }
+
+var TypenameDSL = func() {
+	var _ = API("test", func() {
+		Server("test", func() {
+			Host("localhost", func() {
+				URI("https://goa.design")
+			})
+		})
+	})
+
+	var Foo = Type("Foo", func() {
+		Meta("openapi:typename", "FooPayload")
+		Attribute("value", String, func() {
+			Example("")
+		})
+	})
+
+	var Bar = ResultType("application/vnd.goa.example.bar", func() {
+		TypeName("Bar")
+		Meta("openapi:typename", "BarResult")
+		Attribute("value", String, func() {
+			Example("")
+		})
+	})
+
+	var _ = Service("testService", func() {
+		Method("foo", func() {
+			Payload(Foo)
+			Result(Bar, func() {
+				Meta("openapi:typename", "FooResult")
+			})
+			HTTP(func() {
+				POST("/foo")
+			})
+		})
+		Method("bar", func() {
+			Payload(Foo, func() {
+				Meta("openapi:typename", "BarPayload")
+			})
+			Result(Bar)
+			HTTP(func() {
+				POST("/bar")
+			})
+		})
+		Method("baz", func() {
+			Payload(func() {
+				Meta("openapi:typename", "BazPayload")
+				Attribute("value", String, func() {
+					Example("")
+				})
+			})
+			Result(func() {
+				Meta("openapi:typename", "BazResult")
+				Attribute("value", String, func() {
+					Example("")
+				})
+			})
+			HTTP(func() {
+				POST("/baz")
+			})
+		})
+	})
+}