Skip to content

Commit 08ba92c

Browse files
author
Ansh Chaturvedi
committed
feat: initial impl. of union consolidation
Ticket: DX-614
1 parent add9885 commit 08ba92c

File tree

3 files changed

+84
-20
lines changed

3 files changed

+84
-20
lines changed

packages/openapi-generator/src/ir.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,9 @@ export type SchemaMetadata = Omit<
8282
| 'externalDocs'
8383
>;
8484

85-
export type Schema = BaseSchema & HasComment & SchemaMetadata;
85+
type ExtendedSchemaMetadata = SchemaMetadata & {
86+
primitive?: boolean;
87+
decodedType?: string;
88+
};
89+
90+
export type Schema = BaseSchema & HasComment & ExtendedSchemaMetadata;

packages/openapi-generator/src/knownImports.ts

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ export const KNOWN_IMPORTS: KnownImports = {
4646
},
4747
},
4848
'io-ts': {
49-
string: () => E.right({ type: 'string' }),
50-
number: () => E.right({ type: 'number' }),
49+
string: () => E.right({ type: 'string', primitive: true }),
50+
number: () => E.right({ type: 'number', primitive: true }),
5151
bigint: () => E.right({ type: 'number' }),
52-
boolean: () => E.right({ type: 'boolean' }),
52+
boolean: () => E.right({ type: 'boolean', primitive: true }),
5353
null: () => E.right({ type: 'null' }),
5454
nullType: () => E.right({ type: 'null' }),
5555
undefined: () => E.right({ type: 'undefined' }),
@@ -143,11 +143,13 @@ export const KNOWN_IMPORTS: KnownImports = {
143143
type: 'string',
144144
format: 'number',
145145
pattern: '^\\d+$',
146+
decodedType: 'number',
146147
}),
147148
NaturalFromString: () =>
148149
E.right({
149150
type: 'string',
150151
format: 'number',
152+
decodedType: 'number',
151153
}),
152154
Negative: () =>
153155
E.right({
@@ -161,6 +163,7 @@ export const KNOWN_IMPORTS: KnownImports = {
161163
format: 'number',
162164
maximum: 0,
163165
exclusiveMaximum: true,
166+
decodedType: 'number',
164167
}),
165168
NegativeInt: () =>
166169
E.right({
@@ -174,6 +177,7 @@ export const KNOWN_IMPORTS: KnownImports = {
174177
format: 'number',
175178
maximum: 0,
176179
exclusiveMaximum: true,
180+
decodedType: 'number',
177181
}),
178182
NonNegative: () =>
179183
E.right({
@@ -185,6 +189,7 @@ export const KNOWN_IMPORTS: KnownImports = {
185189
type: 'string',
186190
format: 'number',
187191
minimum: 0,
192+
decodedType: 'number',
188193
}),
189194
NonNegativeInt: () =>
190195
E.right({
@@ -195,6 +200,7 @@ export const KNOWN_IMPORTS: KnownImports = {
195200
E.right({
196201
type: 'string',
197202
format: 'number',
203+
decodedType: 'number',
198204
}),
199205
NonPositive: () =>
200206
E.right({
@@ -206,6 +212,7 @@ export const KNOWN_IMPORTS: KnownImports = {
206212
type: 'string',
207213
format: 'number',
208214
maximum: 0,
215+
decodedType: 'number',
209216
}),
210217
NonPositiveInt: () =>
211218
E.right({
@@ -217,6 +224,7 @@ export const KNOWN_IMPORTS: KnownImports = {
217224
type: 'string',
218225
format: 'number',
219226
maximum: 0,
227+
decodedType: 'number',
220228
}),
221229
NonZero: () =>
222230
E.right({
@@ -226,6 +234,7 @@ export const KNOWN_IMPORTS: KnownImports = {
226234
E.right({
227235
type: 'string',
228236
format: 'number',
237+
decodedType: 'number',
229238
}),
230239
NonZeroInt: () =>
231240
E.right({
@@ -235,6 +244,7 @@ export const KNOWN_IMPORTS: KnownImports = {
235244
E.right({
236245
type: 'string',
237246
format: 'number',
247+
decodedType: 'number',
238248
}),
239249
Positive: () =>
240250
E.right({
@@ -248,6 +258,7 @@ export const KNOWN_IMPORTS: KnownImports = {
248258
format: 'number',
249259
minimum: 0,
250260
exclusiveMinimum: true,
261+
decodedType: 'number',
251262
}),
252263
Zero: () =>
253264
E.right({
@@ -257,13 +268,15 @@ export const KNOWN_IMPORTS: KnownImports = {
257268
E.right({
258269
type: 'string',
259270
format: 'number',
271+
decodedType: 'number',
260272
}),
261273
},
262274
'io-ts-bigint': {
263275
BigIntFromString: () =>
264276
E.right({
265277
type: 'string',
266278
format: 'number',
279+
decodedType: 'bigint',
267280
}),
268281
NegativeBigInt: () =>
269282
E.right({
@@ -275,6 +288,7 @@ export const KNOWN_IMPORTS: KnownImports = {
275288
type: 'string',
276289
format: 'number',
277290
maximum: -1,
291+
decodedType: 'bigint',
278292
}),
279293
NonEmptyString: () => E.right({ type: 'string', minLength: 1 }),
280294
NonNegativeBigInt: () => E.right({ type: 'number', minimum: 0 }),
@@ -283,6 +297,7 @@ export const KNOWN_IMPORTS: KnownImports = {
283297
type: 'string',
284298
format: 'number',
285299
maximum: 0,
300+
decodedType: 'bigint',
286301
}),
287302
NonPositiveBigInt: () =>
288303
E.right({
@@ -294,28 +309,36 @@ export const KNOWN_IMPORTS: KnownImports = {
294309
type: 'string',
295310
format: 'number',
296311
maximum: 0,
312+
decodedType: 'bigint',
297313
}),
298314
NonZeroBigInt: () => E.right({ type: 'number' }),
299315
NonZeroBigIntFromString: () =>
300316
E.right({
301317
type: 'string',
302318
format: 'number',
319+
decodedType: 'bigint',
303320
}),
304321
PositiveBigInt: () => E.right({ type: 'number', minimum: 1 }),
305322
PositiveBigIntFromString: () =>
306323
E.right({
307324
type: 'string',
308325
format: 'number',
309326
minimum: 1,
327+
decodedType: 'bigint',
310328
}),
311329
ZeroBigInt: () => E.right({ type: 'number' }),
312-
ZeroBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
330+
ZeroBigIntFromString: () =>
331+
E.right({ type: 'string', format: 'number', decodedType: 'bigint' }),
313332
},
314333
'io-ts-types': {
315-
NumberFromString: () => E.right({ type: 'string', format: 'number' }),
316-
BigIntFromString: () => E.right({ type: 'string', format: 'number' }),
317-
BooleanFromNumber: () => E.right({ type: 'number', enum: [0, 1] }),
318-
BooleanFromString: () => E.right({ type: 'string', enum: ['true', 'false'] }),
334+
NumberFromString: () =>
335+
E.right({ type: 'string', format: 'number', decodedType: 'number' }),
336+
BigIntFromString: () =>
337+
E.right({ type: 'string', format: 'number', decodedType: 'bigint' }),
338+
BooleanFromNumber: () =>
339+
E.right({ type: 'number', enum: [0, 1], decodedType: 'boolean' }),
340+
BooleanFromString: () =>
341+
E.right({ type: 'string', enum: ['true', 'false'], decodedType: 'boolean' }),
319342
DateFromISOString: () =>
320343
E.right({ type: 'string', format: 'date-time', title: 'ISO Date String' }),
321344
DateFromNumber: () =>
@@ -332,7 +355,8 @@ export const KNOWN_IMPORTS: KnownImports = {
332355
format: 'number',
333356
description: 'Number of seconds since the Unix epoch',
334357
}),
335-
IntFromString: () => E.right({ type: 'string', format: 'integer' }),
358+
IntFromString: () =>
359+
E.right({ type: 'string', format: 'integer', decodedType: 'number' }),
336360
JsonFromString: () => E.right({ type: 'string', title: 'JSON String' }),
337361
nonEmptyArray: (_, innerSchema) =>
338362
E.right({ type: 'array', items: innerSchema, minItems: 1 }),

packages/openapi-generator/src/optimize.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,48 @@ export function foldIntersection(schema: Schema, optimize: OptimizeFn): Schema {
3131
return result;
3232
}
3333

34+
function consolidateUnion(schema: Schema): Schema {
35+
if (schema.type !== 'union') return schema;
36+
if (schema.schemas.length === 1) return schema.schemas[0]!;
37+
if (schema.schemas.length === 0) return { type: 'undefined' };
38+
39+
const consolidatableTypes = ['boolean', 'number', 'string'];
40+
const innerSchemas = schema.schemas.map(optimize);
41+
42+
const isConsolidatableType = (s: Schema): boolean => {
43+
return (
44+
(s.primitive && consolidatableTypes.includes(s.type)) ||
45+
(s.decodedType !== undefined && consolidatableTypes.includes(s.decodedType))
46+
);
47+
};
48+
49+
/**
50+
* We need to check three things:
51+
* 1. All the schemas satisfy isConsolidatableType
52+
* 2. All the schemas have the same decodedType type (aka type at runtime, or the `A` type of the codec)
53+
* 3. At least one of the schemas is a primitive type
54+
*
55+
* If all these conditions are satisfied, we can prove to ourselves that this is a union that
56+
* we can consolidate to the decodedType (runtime) type.
57+
*/
58+
59+
const allConsolidatable = innerSchemas.every(isConsolidatableType);
60+
const hasPrimitive = innerSchemas.some((s: Schema) => s.primitive);
61+
62+
const innerSchemaTypes = new Set(innerSchemas.map((s) => s.decodedType || s.type));
63+
const areSameRuntimeType = innerSchemaTypes.size === 1;
64+
65+
if (allConsolidatable && areSameRuntimeType && hasPrimitive) {
66+
return { type: Array.from(innerSchemaTypes)[0] as Primitive['type'] };
67+
} else {
68+
return schema;
69+
}
70+
}
71+
3472
function mergeUnions(schema: Schema): Schema {
3573
if (schema.type !== 'union') return schema;
36-
else if (schema.schemas.length === 1) return schema.schemas[0]!;
37-
else if (schema.schemas.length === 0) return { type: 'undefined' };
74+
if (schema.schemas.length === 1) return schema.schemas[0]!;
75+
if (schema.schemas.length === 0) return { type: 'undefined' };
3876

3977
// Stringified schemas (i.e. hashes of the schemas) to avoid duplicates
4078
const resultingSchemas: Set<string> = new Set();
@@ -75,13 +113,9 @@ function mergeUnions(schema: Schema): Schema {
75113
}
76114

77115
export function simplifyUnion(schema: Schema, optimize: OptimizeFn): Schema {
78-
if (schema.type !== 'union') {
79-
return schema;
80-
} else if (schema.schemas.length === 1) {
81-
return schema.schemas[0]!;
82-
} else if (schema.schemas.length === 0) {
83-
return { type: 'undefined' };
84-
}
116+
if (schema.type !== 'union') return schema;
117+
if (schema.schemas.length === 1) return schema.schemas[0]!;
118+
if (schema.schemas.length === 0) return { type: 'undefined' };
85119

86120
const innerSchemas = schema.schemas.map(optimize);
87121

@@ -176,7 +210,8 @@ export function optimize(schema: Schema): Schema {
176210
}
177211
return newSchema;
178212
} else if (schema.type === 'union') {
179-
const simplified = simplifyUnion(schema, optimize);
213+
const consolidated = consolidateUnion(schema);
214+
const simplified = simplifyUnion(consolidated, optimize);
180215
const merged = mergeUnions(simplified);
181216

182217
if (schema.comment) {

0 commit comments

Comments
 (0)