Skip to content

Commit 48cf8ab

Browse files
committed
fix(codegen): flatten allOf models and avoid inline enum parameter types
Flatten component schemas that use `allOf` into the generated component model instead of producing wrapper records like `Merchant` around `Merchant2`. Also generate inline query/header enum parameters as `String` or `List<String>` unless they reference a named component schema. This removes awkward SDK-facing types such as `Order2`, `Format2`, `TypesItem2`, and `StatusesItem2` while keeping the implementation low-maintenance and spec-driven.
1 parent c93c8e8 commit 48cf8ab

34 files changed

Lines changed: 3360 additions & 476 deletions

codegen/internal/generator/model.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ type sdkModel struct {
2121

2222
// clientModel encapsulates the data necessary to render a Java API client.
2323
type clientModel struct {
24-
TagName string
24+
TagName string
2525
TagDescriptionLines []string
26-
ClassName string
27-
AccessorName string
28-
FieldName string
29-
Package string
30-
Methods []operationModel
26+
ClassName string
27+
AccessorName string
28+
FieldName string
29+
Package string
30+
Methods []operationModel
3131
}
3232

3333
// operationModel stores the derived metadata for one OpenAPI operation.
@@ -375,7 +375,7 @@ func filterParams(params []*v3.Parameter, location string, resolver *typeResolve
375375
}
376376
seen[name] = struct{}{}
377377
schemaRef := parameterSchema(param)
378-
javaType := resolver.javaType(schemaRef, name)
378+
javaType := resolver.parameterJavaType(schemaRef, name)
379379
required := param.Required != nil && *param.Required
380380
filtered = append(filtered, parameterModel{
381381
Name: name,
@@ -630,7 +630,7 @@ func buildSchemaFields(name string, ref *base.SchemaProxy, resolver *typeResolve
630630
if schema == nil {
631631
return []schemaField{{Name: "value", Type: "Object"}}, nil, nil, false
632632
}
633-
if schemaHasType(schema, "object") {
633+
if schemaDefinesModelFields(schema) {
634634
props := collectProperties(schema)
635635
if len(props) == 0 {
636636
return []schemaField{{Name: "value", Type: "java.util.Map<String, Object>"}}, nil, []string{"java.util.Map"}, false
@@ -695,6 +695,31 @@ func buildSchemaFields(name string, ref *base.SchemaProxy, resolver *typeResolve
695695
return fields, additionalProps, sortedImports(imports), hasRequired
696696
}
697697

698+
func schemaDefinesModelFields(schema *base.Schema) bool {
699+
if schema == nil {
700+
return false
701+
}
702+
return schemaHasType(schema, "object") || schemaHasFlattenableProperties(schema)
703+
}
704+
705+
func schemaHasFlattenableProperties(schema *base.Schema) bool {
706+
if schema == nil {
707+
return false
708+
}
709+
if schema.Properties != nil && schema.Properties.Len() > 0 {
710+
return true
711+
}
712+
for _, item := range schema.AllOf {
713+
if item == nil {
714+
continue
715+
}
716+
if schemaHasFlattenableProperties(item.Schema()) {
717+
return true
718+
}
719+
}
720+
return false
721+
}
722+
698723
// resolveAdditionalProperties returns metadata for schemas that allow
699724
// additional fields alongside declared properties.
700725
func resolveAdditionalProperties(schema *base.Schema, resolver *typeResolver, context ...string) *additionalPropertiesModel {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package generator
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func TestGenerateModelFlattensAllOfIntoComponent(t *testing.T) {
11+
t.Parallel()
12+
13+
tmp := t.TempDir()
14+
specPath := filepath.Join(tmp, "openapi.json")
15+
outputDir := filepath.Join(tmp, "src", "main", "java")
16+
resourceDir := filepath.Join(tmp, "src", "main", "resources")
17+
18+
spec := `{
19+
"openapi": "3.0.3",
20+
"info": {
21+
"title": "test",
22+
"version": "1.0.0"
23+
},
24+
"paths": {},
25+
"components": {
26+
"schemas": {
27+
"Merchant": {
28+
"allOf": [
29+
{
30+
"type": "object",
31+
"required": ["merchant_code"],
32+
"properties": {
33+
"merchant_code": { "type": "string" }
34+
}
35+
},
36+
{
37+
"$ref": "#/components/schemas/Timestamps"
38+
}
39+
],
40+
"title": "Merchant"
41+
},
42+
"Timestamps": {
43+
"type": "object",
44+
"properties": {
45+
"created_at": { "type": "string", "format": "date-time", "readOnly": true }
46+
}
47+
}
48+
}
49+
}
50+
}`
51+
if err := os.WriteFile(specPath, []byte(spec), 0o644); err != nil {
52+
t.Fatalf("write spec: %v", err)
53+
}
54+
55+
params := Params{
56+
SpecPath: specPath,
57+
OutputDir: outputDir,
58+
ResourceDir: resourceDir,
59+
BasePackage: "com.test.sdk",
60+
}
61+
if err := Run(context.Background(), params); err != nil {
62+
t.Fatalf("run generator: %v", err)
63+
}
64+
65+
merchantPath := filepath.Join(outputDir, "com", "test", "sdk", "models", "Merchant.java")
66+
content, err := os.ReadFile(merchantPath)
67+
if err != nil {
68+
t.Fatalf("read generated Merchant model: %v", err)
69+
}
70+
generated := string(content)
71+
72+
assertContains(t, generated, "public record Merchant(")
73+
assertContains(t, generated, "String merchantCode")
74+
assertContains(t, generated, "java.time.OffsetDateTime createdAt")
75+
assertNotContains(t, generated, "Merchant2")
76+
assertFileDoesNotExist(t, filepath.Join(outputDir, "com", "test", "sdk", "models", "Merchant2.java"))
77+
}
78+
79+
func TestGenerateClientUsesStringsForInlineParameterEnums(t *testing.T) {
80+
t.Parallel()
81+
82+
tmp := t.TempDir()
83+
specPath := filepath.Join(tmp, "openapi.json")
84+
outputDir := filepath.Join(tmp, "src", "main", "java")
85+
resourceDir := filepath.Join(tmp, "src", "main", "resources")
86+
87+
spec := `{
88+
"openapi": "3.0.3",
89+
"info": {
90+
"title": "test",
91+
"version": "1.0.0"
92+
},
93+
"paths": {
94+
"/v1/transactions": {
95+
"get": {
96+
"operationId": "ListTransactions",
97+
"parameters": [
98+
{
99+
"name": "order",
100+
"in": "query",
101+
"schema": {
102+
"type": "string",
103+
"enum": ["ascending", "descending"]
104+
}
105+
},
106+
{
107+
"name": "types",
108+
"in": "query",
109+
"schema": {
110+
"type": "array",
111+
"items": {
112+
"type": "string",
113+
"enum": ["PAYMENT", "REFUND"]
114+
}
115+
}
116+
}
117+
],
118+
"responses": {
119+
"204": {
120+
"description": "No content"
121+
}
122+
},
123+
"tags": ["Transactions"]
124+
}
125+
}
126+
}
127+
}`
128+
if err := os.WriteFile(specPath, []byte(spec), 0o644); err != nil {
129+
t.Fatalf("write spec: %v", err)
130+
}
131+
132+
params := Params{
133+
SpecPath: specPath,
134+
OutputDir: outputDir,
135+
ResourceDir: resourceDir,
136+
BasePackage: "com.test.sdk",
137+
}
138+
if err := Run(context.Background(), params); err != nil {
139+
t.Fatalf("run generator: %v", err)
140+
}
141+
142+
clientPath := filepath.Join(outputDir, "com", "test", "sdk", "clients", "TransactionsClient.java")
143+
content, err := os.ReadFile(clientPath)
144+
if err != nil {
145+
t.Fatalf("read generated Transactions client: %v", err)
146+
}
147+
generated := string(content)
148+
149+
assertContains(t, generated, "public ListTransactionsQueryParams order(String value)")
150+
assertContains(t, generated, "public ListTransactionsQueryParams types(java.util.List<String> value)")
151+
assertNotContains(t, generated, "com.test.sdk.models.Order")
152+
assertNotContains(t, generated, "com.test.sdk.models.TypesItem")
153+
assertFileDoesNotExist(t, filepath.Join(outputDir, "com", "test", "sdk", "models", "Order.java"))
154+
assertFileDoesNotExist(t, filepath.Join(outputDir, "com", "test", "sdk", "models", "TypesItem.java"))
155+
}
156+
157+
func assertFileDoesNotExist(t *testing.T, path string) {
158+
t.Helper()
159+
if _, err := os.Stat(path); err == nil {
160+
t.Fatalf("expected %s to not exist", path)
161+
} else if !os.IsNotExist(err) {
162+
t.Fatalf("stat %s: %v", path, err)
163+
}
164+
}

codegen/internal/generator/types.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,8 @@ func (r *typeResolver) javaType(ref *base.SchemaProxy, context ...string) javaTy
146146
if schema.Properties != nil && schema.Properties.Len() > 0 {
147147
return r.inlineObjectType(schema, context)
148148
}
149-
if len(schema.AllOf) > 0 {
150-
for _, item := range schema.AllOf {
151-
if item == nil {
152-
continue
153-
}
154-
return r.javaType(item, context...)
155-
}
149+
if schemaHasFlattenableProperties(schema) {
150+
return r.inlineObjectType(schema, context)
156151
}
157152
if len(schema.OneOf) > 0 {
158153
for _, item := range schema.OneOf {
@@ -173,6 +168,35 @@ func (r *typeResolver) javaType(ref *base.SchemaProxy, context ...string) javaTy
173168
return r.genericMap()
174169
}
175170

171+
func (r *typeResolver) parameterJavaType(ref *base.SchemaProxy, context ...string) javaType {
172+
if ref == nil || ref.IsReference() {
173+
return r.javaType(ref, context...)
174+
}
175+
schema := ref.Schema()
176+
if schema == nil {
177+
return r.genericMap()
178+
}
179+
if _, ok := enumValuesForSchema(schema); ok {
180+
return r.stringType(schema)
181+
}
182+
if (schemaHasType(schema, "array") || (schema.Items != nil && schema.Items.IsA())) &&
183+
schema.Items != nil &&
184+
schema.Items.IsA() &&
185+
schema.Items.A != nil &&
186+
!schema.Items.A.IsReference() {
187+
if itemSchema := schema.Items.A.Schema(); itemSchema != nil {
188+
if _, ok := enumValuesForSchema(itemSchema); ok {
189+
return javaType{
190+
Name: "java.util.List<String>",
191+
Imports: []string{"java.util.List"},
192+
TypeRefExpr: "new TypeReference<java.util.List<String>>() {}",
193+
}
194+
}
195+
}
196+
}
197+
return r.javaType(ref, context...)
198+
}
199+
176200
// objectType handles schemas that look like objects by either emitting inline
177201
// models or falling back to generic map types.
178202
func (r *typeResolver) objectType(schema *base.Schema, context []string) javaType {

src/main/java/com/sumup/sdk/clients/MembershipsAsyncClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public ListMembershipsQueryParams resourceParentId(String value) {
182182
* Pass explicit null to filter for resources without a parent.
183183
* @return This ListMembershipsQueryParams instance.
184184
*/
185-
public ListMembershipsQueryParams resourceParentType(com.sumup.sdk.models.ResourceType value) {
185+
public ListMembershipsQueryParams resourceParentType(java.util.Map<String, Object> value) {
186186
this.values.put("resource.parent.type", Objects.requireNonNull(value, "resourceParentType"));
187187
return this;
188188
}

src/main/java/com/sumup/sdk/clients/MembershipsClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public ListMembershipsQueryParams resourceParentId(String value) {
177177
* Pass explicit null to filter for resources without a parent.
178178
* @return This ListMembershipsQueryParams instance.
179179
*/
180-
public ListMembershipsQueryParams resourceParentType(com.sumup.sdk.models.ResourceType value) {
180+
public ListMembershipsQueryParams resourceParentType(java.util.Map<String, Object> value) {
181181
this.values.put("resource.parent.type", Objects.requireNonNull(value, "resourceParentType"));
182182
return this;
183183
}

src/main/java/com/sumup/sdk/clients/PayoutsAsyncClient.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ public static final class ListPayoutsV1QueryParams {
216216
* @param value Response format for the payout list.
217217
* @return This ListPayoutsV1QueryParams instance.
218218
*/
219-
public ListPayoutsV1QueryParams format(com.sumup.sdk.models.Format value) {
219+
public ListPayoutsV1QueryParams format(String value) {
220220
this.values.put("format", Objects.requireNonNull(value, "format"));
221221
return this;
222222
}
@@ -238,7 +238,7 @@ public ListPayoutsV1QueryParams limit(Long value) {
238238
* @param value Sort direction for the returned payouts.
239239
* @return This ListPayoutsV1QueryParams instance.
240240
*/
241-
public ListPayoutsV1QueryParams order(com.sumup.sdk.models.Order2 value) {
241+
public ListPayoutsV1QueryParams order(String value) {
242242
this.values.put("order", Objects.requireNonNull(value, "order"));
243243
return this;
244244
}
@@ -263,7 +263,7 @@ public static final class ListPayoutsQueryParams {
263263
* @param value Response format for the payout list.
264264
* @return This ListPayoutsQueryParams instance.
265265
*/
266-
public ListPayoutsQueryParams format(com.sumup.sdk.models.Format2 value) {
266+
public ListPayoutsQueryParams format(String value) {
267267
this.values.put("format", Objects.requireNonNull(value, "format"));
268268
return this;
269269
}
@@ -285,7 +285,7 @@ public ListPayoutsQueryParams limit(Long value) {
285285
* @param value Sort direction for the returned payouts.
286286
* @return This ListPayoutsQueryParams instance.
287287
*/
288-
public ListPayoutsQueryParams order(com.sumup.sdk.models.Order2 value) {
288+
public ListPayoutsQueryParams order(String value) {
289289
this.values.put("order", Objects.requireNonNull(value, "order"));
290290
return this;
291291
}

src/main/java/com/sumup/sdk/clients/PayoutsClient.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ public static final class ListPayoutsV1QueryParams {
215215
* @param value Response format for the payout list.
216216
* @return This ListPayoutsV1QueryParams instance.
217217
*/
218-
public ListPayoutsV1QueryParams format(com.sumup.sdk.models.Format value) {
218+
public ListPayoutsV1QueryParams format(String value) {
219219
this.values.put("format", Objects.requireNonNull(value, "format"));
220220
return this;
221221
}
@@ -237,7 +237,7 @@ public ListPayoutsV1QueryParams limit(Long value) {
237237
* @param value Sort direction for the returned payouts.
238238
* @return This ListPayoutsV1QueryParams instance.
239239
*/
240-
public ListPayoutsV1QueryParams order(com.sumup.sdk.models.Order2 value) {
240+
public ListPayoutsV1QueryParams order(String value) {
241241
this.values.put("order", Objects.requireNonNull(value, "order"));
242242
return this;
243243
}
@@ -262,7 +262,7 @@ public static final class ListPayoutsQueryParams {
262262
* @param value Response format for the payout list.
263263
* @return This ListPayoutsQueryParams instance.
264264
*/
265-
public ListPayoutsQueryParams format(com.sumup.sdk.models.Format2 value) {
265+
public ListPayoutsQueryParams format(String value) {
266266
this.values.put("format", Objects.requireNonNull(value, "format"));
267267
return this;
268268
}
@@ -284,7 +284,7 @@ public ListPayoutsQueryParams limit(Long value) {
284284
* @param value Sort direction for the returned payouts.
285285
* @return This ListPayoutsQueryParams instance.
286286
*/
287-
public ListPayoutsQueryParams order(com.sumup.sdk.models.Order2 value) {
287+
public ListPayoutsQueryParams order(String value) {
288288
this.values.put("order", Objects.requireNonNull(value, "order"));
289289
return this;
290290
}

0 commit comments

Comments
 (0)