Repository: goadesign/goa
Pull request #3205 (2022-11-28 11:02:19)

Bump github.com/getkin/kin-openapi from 0.108.0 to 0.109.0
Bumps [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) from 0.108.0 to 0.109.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/getkin/kin-openapi/releases">github.com/getkin/kin-openapi's releases</a>.</em></p>
<blockquote>
<h2>v0.109.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add CodeQL workflow for GitHub code scanning by <a href="https://github.com/lgtm-com"><code>@​lgtm-com</code></a> in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/661">getkin/kin-openapi#661</a></li>
<li>Support x-nullable by <a href="https://github.com/krak3n"><code>@​krak3n</code></a> in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/670">getkin/kin-openapi#670</a></li>
<li>Update content length after replacing request body by <a href="https://github.com/borodiychuk"><code>@​borodiychuk</code></a> in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/672">getkin/kin-openapi#672</a></li>
<li>fix: optional defaults by <a href="https://github.com/orensolo"><code>@​orensolo</code></a> in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/662">getkin/kin-openapi#662</a></li>
<li>fix: wrap the error that came back from the callback (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/674">#674</a>) by <a href="https://github.com/micronull"><code>@​micronull</code></a> in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/675">getkin/kin-openapi#675</a></li>
<li>fix: openapi3.SchemaError message customize (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/678">#678</a>) by <a href="https://github.com/micronull"><code>@​micronull</code></a> in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/679">getkin/kin-openapi#679</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/lgtm-com"><code>@​lgtm-com</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/661">getkin/kin-openapi#661</a></li>
<li><a href="https://github.com/krak3n"><code>@​krak3n</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/670">getkin/kin-openapi#670</a></li>
<li><a href="https://github.com/borodiychuk"><code>@​borodiychuk</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/672">getkin/kin-openapi#672</a></li>
<li><a href="https://github.com/orensolo"><code>@​orensolo</code></a> made their first contribution in <a href="https://github-redirect.dependabot.com/getkin/kin-openapi/pull/662">getkin/kin-openapi#662</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/getkin/kin-openapi/compare/v0.108.0...v0.109.0">https://github.com/getkin/kin-openapi/compare/v0.108.0...v0.109.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/getkin/kin-openapi/commit/cd217fe551cc36fcc74326076ec9fa0a38cc0920"><code>cd217fe</code></a> fix: openapi3.SchemaError message customize (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/678">#678</a>) (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/679">#679</a>)</li>
<li><a href="https://github.com/getkin/kin-openapi/commit/0d3c1791c081407d63391f0e526c21b50b140f2b"><code>0d3c179</code></a> fix: wrap the error that came back from the callback (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/674">#674</a>) (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/675">#675</a>)</li>
<li><a href="https://github.com/getkin/kin-openapi/commit/4a7405dd77894e637300b94035f3ecfde3fe286a"><code>4a7405d</code></a> fix: optional defaults (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/662">#662</a>)</li>
<li><a href="https://github.com/getkin/kin-openapi/commit/657743e5310eb4db0621fd493f0d241764c3f4c9"><code>657743e</code></a> Update content length after replacing request body (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/672">#672</a>)</li>
<li><a href="https://github.com/getkin/kin-openapi/commit/cadbddacb449522cd43dc78706eca9a8e554827c"><code>cadbdda</code></a> Support x-nullable (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/670">#670</a>)</li>
<li><a href="https://github.com/getkin/kin-openapi/commit/871e029d62a4d47239d11006431407e08edb1259"><code>871e029</code></a> Add CodeQL workflow for GitHub code scanning (<a href="https://github-redirect.dependabot.com/getkin/kin-openapi/issues/661">#661</a>)</li>
<li>See full diff in <a href="https://github.com/getkin/kin-openapi/compare/v0.108.0...v0.109.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/getkin/kin-openapi&package-manager=go_modules&previous-version=0.108.0&new-version=0.109.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


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/go.mod b/go.mod
index a28988ca..1cc37f03 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.19
 require (
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
 	github.com/dimfeld/httptreemux/v5 v5.5.0
-	github.com/getkin/kin-openapi v0.108.0
+	github.com/getkin/kin-openapi v0.109.0
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/websocket v1.5.0
 	github.com/hashicorp/go-multierror v1.1.1
diff --git a/go.sum b/go.sum
index 90216105..63f04b5f 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZs
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI=
 github.com/dimfeld/httptreemux/v5 v5.5.0 h1:p8jkiMrCuZ0CmhwYLcbNbl7DDo21fozhKHQ2PccwOFQ=
 github.com/dimfeld/httptreemux/v5 v5.5.0/go.mod h1:QeEylH57C0v3VO0tkKraVz9oD3Uu93CKPnTLbsidvSw=
-github.com/getkin/kin-openapi v0.108.0 h1:EYf0GtsKa4hQNIlplGS+Au7NEfGQ1F7MoHD2kcVevPQ=
-github.com/getkin/kin-openapi v0.108.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8=
+github.com/getkin/kin-openapi v0.109.0 h1:Cpb0PmIPFEV0LVvikEvfo3gw3rBMVSjJ57w15j+/A/U=
+github.com/getkin/kin-openapi v0.109.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8=
 github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
 github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
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")
+			})
+		})
+	})
+}