Skip to content

Commit df1919c

Browse files
authored
Add "openapi:typename" meta to user types (#3572)
This update forces OpenAPI generators to use specified type names rather than merging structurally identical types automatically. Note: Goa-generated types (e.g., from anonymous payload or result definitions) will still be merged if structurally identical. This approach helps prevent the unnecessary proliferation of types in OpenAPI specifications.
1 parent d9f4dc7 commit df1919c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+226
-280
lines changed

dsl/result_type.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ func ResultType(identifier string, args ...any) *expr.ResultTypeExpr {
116116
}
117117
// Add the type to the generated types root for later evaluation.
118118
rt := expr.NewResultTypeExpr(typeName, identifier, fn)
119+
rt.Meta = expr.MetaExpr{"openapi:typename": []string{typeName}}
119120
expr.Root.ResultTypes = append(expr.Root.ResultTypes, rt)
120121

121122
return rt

dsl/user_type.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,12 @@ func Type(name string, args ...any) expr.UserType {
103103
}
104104

105105
t := &expr.UserTypeExpr{
106-
TypeName: name,
107-
AttributeExpr: &expr.AttributeExpr{Type: base, DSLFunc: fn},
106+
TypeName: name,
107+
AttributeExpr: &expr.AttributeExpr{
108+
Type: base,
109+
DSLFunc: fn,
110+
Meta: expr.MetaExpr{"openapi:typename": []string{name}},
111+
},
108112
}
109113
expr.Root.Types = append(expr.Root.Types, t)
110114
return t

http/codegen/openapi/v2/files_test.go

Lines changed: 29 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/getkin/kin-openapi/openapi2"
1515
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
1617

1718
httpgen "goa.design/goa/v3/http/codegen"
1819
"goa.design/goa/v3/http/codegen/openapi"
@@ -99,15 +100,15 @@ func TestSections(t *testing.T) {
99100
t.Fatalf("failed to read golden file: %s", err)
100101
}
101102
if !bytes.Equal(buf.Bytes(), want) {
102-
var left, right string
103+
var got, expected string
103104
if filepath.Ext(o.Path) == ".json" {
104-
left = prettifyJSON(t, buf.Bytes())
105-
right = prettifyJSON(t, want)
105+
got = prettifyJSON(t, buf.Bytes())
106+
expected = prettifyJSON(t, want)
106107
} else {
107-
left = buf.String()
108-
right = string(want)
108+
got = buf.String()
109+
expected = string(want)
109110
}
110-
assert.Equal(t, left, right)
111+
assert.Equal(t, expected, got)
111112
}
112113
})
113114
}
@@ -145,51 +146,32 @@ func TestValidations(t *testing.T) {
145146
openapi.Definitions = make(map[string]*openapi.Schema)
146147
root := httpgen.RunHTTPDSL(t, c.DSL)
147148
oFiles, err := openapiv2.Files(root)
148-
if err != nil {
149-
t.Fatalf("OpenAPI failed with %s", err)
150-
}
151-
if len(oFiles) == 0 {
152-
t.Fatalf("No swagger files")
153-
}
149+
require.NoError(t, err, "OpenAPI failed")
150+
require.NotEmpty(t, oFiles, "No swagger files")
154151
for i, o := range oFiles {
155152
tname := fmt.Sprintf("file%d", i)
156153
s := o.SectionTemplates
157154
t.Run(tname, func(t *testing.T) {
158-
if len(s) != 1 {
159-
t.Fatalf("expected 1 section, got %d", len(s))
160-
}
161-
if s[0].Source == "" {
162-
t.Fatalf("empty section template")
163-
}
164-
if s[0].Data == nil {
165-
t.Fatalf("nil data")
166-
}
155+
require.Len(t, s, 1, "expected 1 section, got %d", len(s))
156+
require.NotEmpty(t, s[0].Source, "empty section template")
157+
require.NotNil(t, s[0].Data, "nil data")
167158
var buf bytes.Buffer
168159
tmpl := template.Must(template.New("openapi").Funcs(s[0].FuncMap).Parse(s[0].Source))
169-
if err := tmpl.Execute(&buf, s[0].Data); err != nil {
170-
t.Fatalf("failed to render template: %s", err)
171-
}
160+
require.NoError(t, tmpl.Execute(&buf, s[0].Data), "failed to render template")
172161
if filepath.Ext(o.Path) == ".json" {
173-
if err := validateSwagger(buf.Bytes()); err != nil {
174-
t.Fatalf("invalid swagger: %s", err)
175-
}
162+
require.NoError(t, validateSwagger(buf.Bytes()), "invalid swagger")
176163
}
177164

178165
golden := filepath.Join(goldenPath, fmt.Sprintf("%s_%s.golden", c.Name, tname))
179166
if *update {
180-
if err := os.WriteFile(golden, buf.Bytes(), 0644); err != nil {
181-
t.Fatalf("failed to update golden file: %s", err)
182-
}
167+
require.NoError(t, os.WriteFile(golden, buf.Bytes(), 0644), "failed to update golden file")
168+
return
183169
}
184170

185171
want, err := os.ReadFile(golden)
172+
require.NoError(t, err, "failed to read golden file")
186173
want = bytes.ReplaceAll(want, []byte{'\r', '\n'}, []byte{'\n'})
187-
if err != nil {
188-
t.Fatalf("failed to read golden file: %s", err)
189-
}
190-
if !bytes.Equal(buf.Bytes(), want) {
191-
t.Errorf("result do not match the golden file:\n--BEGIN--\n%s\n--END--\n", buf.Bytes())
192-
}
174+
assert.Equal(t, string(want), buf.String())
193175
})
194176
}
195177
})
@@ -212,51 +194,32 @@ func TestExtensions(t *testing.T) {
212194
openapi.Definitions = make(map[string]*openapi.Schema)
213195
root := httpgen.RunHTTPDSL(t, c.DSL)
214196
oFiles, err := openapiv2.Files(root)
215-
if err != nil {
216-
t.Fatalf("OpenAPI failed with %s", err)
217-
}
218-
if len(oFiles) == 0 {
219-
t.Fatalf("No swagger files")
220-
}
197+
require.NoError(t, err, "OpenAPI failed")
198+
require.NotEmpty(t, oFiles, "No swagger files")
221199
for i, o := range oFiles {
222200
tname := fmt.Sprintf("file%d", i)
223201
s := o.SectionTemplates
224202
t.Run(tname, func(t *testing.T) {
225-
if len(s) != 1 {
226-
t.Fatalf("expected 1 section, got %d", len(s))
227-
}
228-
if s[0].Source == "" {
229-
t.Fatalf("empty section template")
230-
}
231-
if s[0].Data == nil {
232-
t.Fatalf("nil data")
233-
}
203+
require.Len(t, s, 1, "expected 1 section, got %d", len(s))
204+
require.NotEmpty(t, s[0].Source, "empty section template")
205+
require.NotNil(t, s[0].Data, "nil data")
234206
var buf bytes.Buffer
235207
tmpl := template.Must(template.New("openapi").Funcs(s[0].FuncMap).Parse(s[0].Source))
236-
if err := tmpl.Execute(&buf, s[0].Data); err != nil {
237-
t.Fatalf("failed to render template: %s", err)
238-
}
208+
require.NoError(t, tmpl.Execute(&buf, s[0].Data), "failed to render template")
239209
if filepath.Ext(o.Path) == ".json" {
240-
if err := validateSwagger(buf.Bytes()); err != nil {
241-
t.Fatalf("invalid swagger: %s", err)
242-
}
210+
require.NoError(t, validateSwagger(buf.Bytes()), "invalid swagger")
243211
}
244212

245213
golden := filepath.Join(goldenPath, fmt.Sprintf("%s_%s.golden", c.Name, tname))
246214
if *update {
247-
if err := os.WriteFile(golden, buf.Bytes(), 0644); err != nil {
248-
t.Fatalf("failed to update golden file: %s", err)
249-
}
215+
require.NoError(t, os.WriteFile(golden, buf.Bytes(), 0644), "failed to update golden file")
216+
return
250217
}
251218

252219
want, err := os.ReadFile(golden)
253220
want = bytes.ReplaceAll(want, []byte{'\r', '\n'}, []byte{'\n'})
254-
if err != nil {
255-
t.Fatalf("failed to read golden file: %s", err)
256-
}
257-
if !bytes.Equal(buf.Bytes(), want) {
258-
t.Errorf("result do not match the golden file:\n--BEGIN--\n%s\n--END--\n", buf.Bytes())
259-
}
221+
require.NoError(t, err, "failed to read golden file")
222+
assert.Equal(t, string(want), buf.String())
260223
})
261224
}
262225
})
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"swagger":"2.0","info":{"title":"","version":"0.0.1","x-test-api":"API"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"operationId":"testService#testEndpoint","parameters":[{"in":"body","name":"TestEndpointRequestBody","required":true,"schema":{"$ref":"#/definitions/TestServiceTestEndpointRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointResponseBody"}}},"schemes":["https"],"summary":"testEndpoint testService","tags":["testService"],"x-test-operation":"Operation"},"x-test-foo":"bar"}},"definitions":{"TestServiceTestEndpointRequestBody":{"title":"TestServiceTestEndpointRequestBody","type":"object","properties":{"string":{"example":"","type":"string","x-test-schema":"Payload"}},"example":{"string":""}},"TestServiceTestEndpointResponseBody":{"title":"TestServiceTestEndpointResponseBody","type":"object","properties":{"string":{"example":"","type":"string","x-test-schema":"Result"}},"example":{"string":""}}},"tags":[{"description":"Description of Backend","externalDocs":{"description":"See more docs here","url":"http://example.com"},"name":"Backend","x-data":{"foo":"bar"}}]}
1+
{"swagger":"2.0","info":{"title":"","version":"0.0.1","x-test-api":"API"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"post":{"operationId":"testService#testEndpoint","parameters":[{"in":"body","name":"TestEndpointRequestBody","required":true,"schema":{"$ref":"#/definitions/Payload"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Result"}}},"schemes":["https"],"summary":"testEndpoint testService","tags":["testService"],"x-test-operation":"Operation"},"x-test-foo":"bar"}},"definitions":{"Payload":{"title":"Payload","type":"object","properties":{"string":{"example":"","type":"string","x-test-schema":"Payload"}},"example":{"string":""}},"Result":{"title":"Result","type":"object","properties":{"string":{"example":"","type":"string","x-test-schema":"Result"}},"example":{"string":""}}},"tags":[{"description":"Description of Backend","externalDocs":{"description":"See more docs here","url":"http://example.com"},"name":"Backend","x-data":{"foo":"bar"}}]}

http/codegen/openapi/v2/testdata/TestExtensions/endpoint_file1.golden

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ paths:
2121
name: TestEndpointRequestBody
2222
required: true
2323
schema:
24-
$ref: '#/definitions/TestServiceTestEndpointRequestBody'
24+
$ref: '#/definitions/Payload'
2525
responses:
2626
"200":
2727
description: OK response.
2828
schema:
29-
$ref: '#/definitions/TestServiceTestEndpointResponseBody'
29+
$ref: '#/definitions/Result'
3030
schemes:
3131
- https
3232
summary: testEndpoint testService
@@ -35,8 +35,8 @@ paths:
3535
x-test-operation: Operation
3636
x-test-foo: bar
3737
definitions:
38-
TestServiceTestEndpointRequestBody:
39-
title: TestServiceTestEndpointRequestBody
38+
Payload:
39+
title: Payload
4040
type: object
4141
properties:
4242
string:
@@ -45,8 +45,8 @@ definitions:
4545
x-test-schema: Payload
4646
example:
4747
string: ""
48-
TestServiceTestEndpointResponseBody:
49-
title: TestServiceTestEndpointResponseBody
48+
Result:
49+
title: Result
5050
type: object
5151
properties:
5252
string:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/TestServiceTestEndpointRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointResponseBody"}}},"schemes":["https"]},"post":{"tags":["anotherTestService"],"summary":"testEndpoint anotherTestService","operationId":"anotherTestService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/AnotherTestServiceTestEndpointRequestBody"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/AnotherTestServiceTestEndpointResponseBody"}}},"schemes":["https"]}}},"definitions":{"AnotherTestServiceTestEndpointRequestBody":{"title":"AnotherTestServiceTestEndpointRequestBody","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""}},"AnotherTestServiceTestEndpointResponseBody":{"title":"AnotherTestServiceTestEndpointResponseBody","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""}},"TestServiceTestEndpointRequestBody":{"title":"TestServiceTestEndpointRequestBody","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""}},"TestServiceTestEndpointResponseBody":{"title":"TestServiceTestEndpointResponseBody","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""}}}}
1+
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"goa.design","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpoint testService","operationId":"testService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/Payload"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Result"}}},"schemes":["https"]},"post":{"tags":["anotherTestService"],"summary":"testEndpoint anotherTestService","operationId":"anotherTestService#testEndpoint","parameters":[{"name":"TestEndpointRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/Payload"}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/Result"}}},"schemes":["https"]}}},"definitions":{"Payload":{"title":"Payload","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""}},"Result":{"title":"Result","type":"object","properties":{"string":{"type":"string","example":""}},"example":{"string":""}}}}

http/codegen/openapi/v2/testdata/TestSections/multiple-services_file1.golden

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ paths:
2323
in: body
2424
required: true
2525
schema:
26-
$ref: '#/definitions/TestServiceTestEndpointRequestBody'
26+
$ref: '#/definitions/Payload'
2727
responses:
2828
"200":
2929
description: OK response.
3030
schema:
31-
$ref: '#/definitions/TestServiceTestEndpointResponseBody'
31+
$ref: '#/definitions/Result'
3232
schemes:
3333
- https
3434
post:
@@ -41,44 +41,26 @@ paths:
4141
in: body
4242
required: true
4343
schema:
44-
$ref: '#/definitions/AnotherTestServiceTestEndpointRequestBody'
44+
$ref: '#/definitions/Payload'
4545
responses:
4646
"200":
4747
description: OK response.
4848
schema:
49-
$ref: '#/definitions/AnotherTestServiceTestEndpointResponseBody'
49+
$ref: '#/definitions/Result'
5050
schemes:
5151
- https
5252
definitions:
53-
AnotherTestServiceTestEndpointRequestBody:
54-
title: AnotherTestServiceTestEndpointRequestBody
53+
Payload:
54+
title: Payload
5555
type: object
5656
properties:
5757
string:
5858
type: string
5959
example: ""
6060
example:
6161
string: ""
62-
AnotherTestServiceTestEndpointResponseBody:
63-
title: AnotherTestServiceTestEndpointResponseBody
64-
type: object
65-
properties:
66-
string:
67-
type: string
68-
example: ""
69-
example:
70-
string: ""
71-
TestServiceTestEndpointRequestBody:
72-
title: TestServiceTestEndpointRequestBody
73-
type: object
74-
properties:
75-
string:
76-
type: string
77-
example: ""
78-
example:
79-
string: ""
80-
TestServiceTestEndpointResponseBody:
81-
title: TestServiceTestEndpointResponseBody
62+
Result:
63+
title: Result
8264
type: object
8365
properties:
8466
string:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpointDefault testService","operationId":"testService#testEndpointDefault","produces":["application/custom+json"],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointDefaultResponseBody"}}},"schemes":["http"]}},"/tiny":{"get":{"tags":["testService"],"summary":"testEndpointTiny testService","operationId":"testService#testEndpointTiny","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointTinyResponseBodyTiny"}}},"schemes":["http"]}}},"definitions":{"TestServiceTestEndpointDefaultResponseBody":{"title":"Mediatype identifier: application/json; view=default","type":"object","properties":{"int":{"type":"integer","example":1,"format":"int64"},"string":{"type":"string","example":""}},"description":"TestEndpointDefaultResponseBody result type (default view)","example":{"int":1,"string":""}},"TestServiceTestEndpointTinyResponseBodyTiny":{"title":"Mediatype identifier: application/json; view=tiny","type":"object","properties":{"string":{"type":"string","example":""}},"description":"TestEndpointTinyResponseBody result type (tiny view) (default view)","example":{"string":""}}}}
1+
{"swagger":"2.0","info":{"title":"","version":"0.0.1"},"host":"localhost:80","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["testService"],"summary":"testEndpointDefault testService","operationId":"testService#testEndpointDefault","produces":["application/custom+json"],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/JSON"}}},"schemes":["http"]}},"/tiny":{"get":{"tags":["testService"],"summary":"testEndpointTiny testService","operationId":"testService#testEndpointTiny","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/TestServiceTestEndpointTinyResponseBodyTiny"}}},"schemes":["http"]}}},"definitions":{"JSON":{"title":"Mediatype identifier: application/json; view=default","type":"object","properties":{"int":{"type":"integer","example":1,"format":"int64"},"string":{"type":"string","example":""}},"description":"TestEndpointDefaultResponseBody result type (default view)","example":{"int":1,"string":""}},"TestServiceTestEndpointTinyResponseBodyTiny":{"title":"Mediatype identifier: application/json; view=tiny","type":"object","properties":{"string":{"type":"string","example":""}},"description":"TestEndpointTinyResponseBody result type (tiny view) (default view)","example":{"string":""}}}}

http/codegen/openapi/v2/testdata/TestSections/multiple-views_file1.golden

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ paths:
2424
"200":
2525
description: OK response.
2626
schema:
27-
$ref: '#/definitions/TestServiceTestEndpointDefaultResponseBody'
27+
$ref: '#/definitions/JSON'
2828
schemes:
2929
- http
3030
/tiny:
@@ -41,7 +41,7 @@ paths:
4141
schemes:
4242
- http
4343
definitions:
44-
TestServiceTestEndpointDefaultResponseBody:
44+
JSON:
4545
title: 'Mediatype identifier: application/json; view=default'
4646
type: object
4747
properties:

0 commit comments

Comments
 (0)