Skip to content

Commit 37f25df

Browse files
committed
Add collapseSingleItemComposite
1 parent e7a5de8 commit 37f25df

2 files changed

Lines changed: 252 additions & 7 deletions

File tree

tools/proto-convert/src/SchemaModifier.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export class SchemaModifier {
1919
this.deduplicateEnumValue(schema)
2020
this.handleAdditionalPropertiesUndefined(schema)
2121
this.convertNullTypeToNullValue(schema)
22-
this.collapseOrMergeOneOfArray(schema)
22+
this.deduplicateOneOfWithArrayType(schema)
23+
this.collapseSingleItemComposite(schema);
2324
this.removeArrayOfMapWrapper(schema)
2425
},
2526
onSchema: (schema, schemaName) => {
@@ -29,7 +30,8 @@ export class SchemaModifier {
2930
this.handleAdditionalPropertiesUndefined(schema)
3031
this.convertNullTypeToNullValue(schema)
3132
this.handleOneOfConst(schema, schemaName)
32-
this.collapseOrMergeOneOfArray(schema)
33+
this.deduplicateOneOfWithArrayType(schema)
34+
this.collapseSingleItemComposite(schema);
3335
this.collapseOneOfObjectPropContainsTitleSchema(schema)
3436
this.removeArrayOfMapWrapper(schema)
3537
this.convertOneOfToMinMaxProperties(schema)
@@ -100,7 +102,7 @@ export class SchemaModifier {
100102
// Simplify schemas with `oneOf` by aggregating items.
101103
// If there are only two `oneOf` items and one matches an array schema, remove oneOf type and set type to array.
102104
// If there are more than two `oneOf` items and one matches an array schema, remove that item from `oneOf`.
103-
collapseOrMergeOneOfArray(schema: OpenAPIV3.SchemaObject): void{
105+
deduplicateOneOfWithArrayType(schema: OpenAPIV3.SchemaObject): void{
104106
if (!('$ref' in schema) && Array.isArray(schema.oneOf)) {
105107
const oneOfs = schema.oneOf;
106108

@@ -122,15 +124,19 @@ export class SchemaModifier {
122124
oneOfs.splice(deleteIndx, 1);
123125
}
124126
}
125-
this.collapseSingleItemOneOf(schema);
126127
}
127128
}
128129

129-
collapseSingleItemOneOf(schema: OpenAPIV3.SchemaObject): void {
130+
collapseSingleItemComposite(schema: OpenAPIV3.SchemaObject): void {
130131
if (Array.isArray(schema.oneOf) && schema.oneOf.length === 1) {
131-
const [singleOneOf] = schema.oneOf as OpenAPIV3.SchemaObject[];
132-
Object.assign(schema, singleOneOf);
132+
Object.assign(schema, schema.oneOf[0]);
133133
delete schema.oneOf;
134+
} else if (Array.isArray(schema.allOf) && schema.allOf.length === 1) {
135+
Object.assign(schema, schema.allOf[0]);
136+
delete schema.allOf;
137+
} else if (Array.isArray(schema.anyOf) && schema.anyOf.length === 1) {
138+
Object.assign(schema, schema.anyOf[0]);
139+
delete schema.anyOf;
134140
}
135141
}
136142

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* Tests for SchemaModifier.
3+
*/
4+
5+
import type { OpenAPIV3 } from 'openapi-types';
6+
import { SchemaModifier } from '../src/SchemaModifier';
7+
8+
describe('SchemaModifier', () => {
9+
// Helper to create a minimal OpenAPI document
10+
const createDocument = (): OpenAPIV3.Document => ({
11+
openapi: '3.0.0',
12+
info: { title: 'Test', version: '1.0.0' },
13+
paths: {},
14+
components: { schemas: {} }
15+
});
16+
17+
describe('deduplicateOneOfWithArrayType', () => {
18+
it('should remove single item when array of same primitive type exists', () => {
19+
const doc = createDocument();
20+
const modifier = new SchemaModifier(doc);
21+
22+
const schema: OpenAPIV3.SchemaObject = {
23+
oneOf: [
24+
{ type: 'string' },
25+
{ type: 'array', items: { type: 'string' } }
26+
]
27+
};
28+
29+
modifier.deduplicateOneOfWithArrayType(schema);
30+
31+
expect(schema.oneOf).toHaveLength(1);
32+
expect(schema.oneOf![0]).toEqual({ type: 'array', items: { type: 'string' } });
33+
});
34+
35+
it('should remove single item when array of same $ref type exists', () => {
36+
const doc = createDocument();
37+
const modifier = new SchemaModifier(doc);
38+
39+
const schema: OpenAPIV3.SchemaObject = {
40+
oneOf: [
41+
{ $ref: '#/components/schemas/MyType' },
42+
{ type: 'array', items: { $ref: '#/components/schemas/MyType' } }
43+
]
44+
};
45+
46+
modifier.deduplicateOneOfWithArrayType(schema);
47+
48+
expect(schema.oneOf).toHaveLength(1);
49+
expect(schema.oneOf![0]).toEqual({
50+
type: 'array',
51+
items: { $ref: '#/components/schemas/MyType' }
52+
});
53+
});
54+
55+
it('should not modify when no duplicate exists', () => {
56+
const doc = createDocument();
57+
const modifier = new SchemaModifier(doc);
58+
59+
const schema: OpenAPIV3.SchemaObject = {
60+
oneOf: [
61+
{ type: 'string' },
62+
{ type: 'array', items: { type: 'integer' } }
63+
]
64+
};
65+
66+
modifier.deduplicateOneOfWithArrayType(schema);
67+
68+
expect(schema.oneOf).toHaveLength(2);
69+
expect(schema.oneOf![0]).toEqual({ type: 'string' });
70+
expect(schema.oneOf![1]).toEqual({ type: 'array', items: { type: 'integer' } });
71+
});
72+
73+
it('should handle additionalProperties in type comparison', () => {
74+
const doc = createDocument();
75+
const modifier = new SchemaModifier(doc);
76+
77+
const schema: OpenAPIV3.SchemaObject = {
78+
oneOf: [
79+
{ type: 'object', additionalProperties: { type: 'string' } },
80+
{ type: 'array', items: { type: 'object', additionalProperties: { type: 'string' } } }
81+
]
82+
};
83+
84+
modifier.deduplicateOneOfWithArrayType(schema);
85+
86+
expect(schema.oneOf).toHaveLength(1);
87+
expect(schema.oneOf![0]).toEqual({
88+
type: 'array',
89+
items: { type: 'object', additionalProperties: { type: 'string' } }
90+
});
91+
});
92+
93+
it('should not remove when additionalProperties differ', () => {
94+
const doc = createDocument();
95+
const modifier = new SchemaModifier(doc);
96+
97+
const schema: OpenAPIV3.SchemaObject = {
98+
oneOf: [
99+
{ type: 'object', additionalProperties: { type: 'string' } },
100+
{ type: 'array', items: { type: 'object', additionalProperties: { type: 'integer' } } }
101+
]
102+
};
103+
104+
modifier.deduplicateOneOfWithArrayType(schema);
105+
106+
expect(schema.oneOf).toHaveLength(2);
107+
});
108+
109+
it('should not modify schema without oneOf', () => {
110+
const doc = createDocument();
111+
const modifier = new SchemaModifier(doc);
112+
113+
const schema: OpenAPIV3.SchemaObject = {
114+
type: 'object',
115+
properties: {
116+
name: { type: 'string' }
117+
}
118+
};
119+
120+
modifier.deduplicateOneOfWithArrayType(schema);
121+
122+
expect(schema.oneOf).toBeUndefined();
123+
expect(schema.type).toBe('object');
124+
});
125+
126+
it('should remove first matching single item when multiple array types exist', () => {
127+
const doc = createDocument();
128+
const modifier = new SchemaModifier(doc);
129+
130+
const schema: OpenAPIV3.SchemaObject = {
131+
oneOf: [
132+
{ type: 'string' },
133+
{ type: 'integer' },
134+
{ type: 'array', items: { type: 'string' } },
135+
{ type: 'array', items: { type: 'integer' } }
136+
]
137+
};
138+
139+
modifier.deduplicateOneOfWithArrayType(schema);
140+
141+
expect(schema.oneOf).toHaveLength(3);
142+
expect(schema.oneOf).toContainEqual({ type: 'integer' });
143+
expect(schema.oneOf).toContainEqual({ type: 'array', items: { type: 'string' } });
144+
expect(schema.oneOf).toContainEqual({ type: 'array', items: { type: 'integer' } });
145+
});
146+
147+
it('should preserve items not matching any array type', () => {
148+
const doc = createDocument();
149+
const modifier = new SchemaModifier(doc);
150+
151+
const schema: OpenAPIV3.SchemaObject = {
152+
oneOf: [
153+
{ type: 'string' },
154+
{ type: 'boolean' },
155+
{ type: 'array', items: { type: 'string' } }
156+
]
157+
};
158+
159+
modifier.deduplicateOneOfWithArrayType(schema);
160+
161+
expect(schema.oneOf).toHaveLength(2);
162+
expect(schema.oneOf).toContainEqual({ type: 'boolean' });
163+
expect(schema.oneOf).toContainEqual({ type: 'array', items: { type: 'string' } });
164+
});
165+
});
166+
167+
describe('collapseSingleItemComposite', () => {
168+
it('should collapse single-item oneOf', () => {
169+
const doc = createDocument();
170+
const modifier = new SchemaModifier(doc);
171+
172+
const schema: OpenAPIV3.SchemaObject = {
173+
oneOf: [{ type: 'string' }]
174+
};
175+
176+
modifier.collapseSingleItemComposite(schema);
177+
178+
expect(schema.oneOf).toBeUndefined();
179+
expect(schema.type).toBe('string');
180+
});
181+
182+
it('should collapse single-item allOf', () => {
183+
const doc = createDocument();
184+
const modifier = new SchemaModifier(doc);
185+
186+
const schema: OpenAPIV3.SchemaObject = {
187+
allOf: [{ type: 'integer' }]
188+
};
189+
190+
modifier.collapseSingleItemComposite(schema);
191+
192+
expect(schema.allOf).toBeUndefined();
193+
expect(schema.type).toBe('integer');
194+
});
195+
196+
it('should collapse single-item anyOf', () => {
197+
const doc = createDocument();
198+
const modifier = new SchemaModifier(doc);
199+
200+
const schema: OpenAPIV3.SchemaObject = {
201+
anyOf: [{ $ref: '#/components/schemas/MyType' }]
202+
};
203+
204+
modifier.collapseSingleItemComposite(schema);
205+
206+
expect(schema.anyOf).toBeUndefined();
207+
expect((schema as any).$ref).toBe('#/components/schemas/MyType');
208+
});
209+
210+
it('should not collapse multi-item oneOf', () => {
211+
const doc = createDocument();
212+
const modifier = new SchemaModifier(doc);
213+
214+
const schema: OpenAPIV3.SchemaObject = {
215+
oneOf: [
216+
{ type: 'string' },
217+
{ type: 'integer' }
218+
]
219+
};
220+
221+
modifier.collapseSingleItemComposite(schema);
222+
223+
expect(schema.oneOf).toHaveLength(2);
224+
});
225+
226+
it('should not modify empty array', () => {
227+
const doc = createDocument();
228+
const modifier = new SchemaModifier(doc);
229+
230+
const schema: OpenAPIV3.SchemaObject = {
231+
oneOf: []
232+
};
233+
234+
modifier.collapseSingleItemComposite(schema);
235+
236+
expect(schema.oneOf).toHaveLength(0);
237+
});
238+
});
239+
});

0 commit comments

Comments
 (0)