Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/openapi-generator/src/ir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ export type SchemaMetadata = Omit<
| 'externalDocs'
>;

export type Schema = BaseSchema & HasComment & SchemaMetadata;
type ExtendedSchemaMetadata = SchemaMetadata & {
Copy link
Contributor

@ad-world ad-world Jul 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe rename this SchemaUtil or something else descriptive that doesn't include Metadata since we use the term metadata to describe the OpenAPI tags in this file, where as these fields are used for something else!

primitive?: boolean;
decodedType?: string;
};

export type Schema = BaseSchema & HasComment & ExtendedSchemaMetadata;
42 changes: 33 additions & 9 deletions packages/openapi-generator/src/knownImports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ export const KNOWN_IMPORTS: KnownImports = {
},
},
'io-ts': {
string: () => E.right({ type: 'string' }),
number: () => E.right({ type: 'number' }),
string: () => E.right({ type: 'string', primitive: true }),
number: () => E.right({ type: 'number', primitive: true }),
bigint: () => E.right({ type: 'number' }),
boolean: () => E.right({ type: 'boolean' }),
boolean: () => E.right({ type: 'boolean', primitive: true }),
null: () => E.right({ type: 'null' }),
nullType: () => E.right({ type: 'null' }),
undefined: () => E.right({ type: 'undefined' }),
Expand Down Expand Up @@ -143,11 +143,13 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
pattern: '^\\d+$',
decodedType: 'number',
}),
NaturalFromString: () =>
E.right({
type: 'string',
format: 'number',
decodedType: 'number',
}),
Negative: () =>
E.right({
Expand All @@ -161,6 +163,7 @@ export const KNOWN_IMPORTS: KnownImports = {
format: 'number',
maximum: 0,
exclusiveMaximum: true,
decodedType: 'number',
}),
NegativeInt: () =>
E.right({
Expand All @@ -174,6 +177,7 @@ export const KNOWN_IMPORTS: KnownImports = {
format: 'number',
maximum: 0,
exclusiveMaximum: true,
decodedType: 'number',
}),
NonNegative: () =>
E.right({
Expand All @@ -185,6 +189,7 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
minimum: 0,
decodedType: 'number',
}),
NonNegativeInt: () =>
E.right({
Expand All @@ -195,6 +200,7 @@ export const KNOWN_IMPORTS: KnownImports = {
E.right({
type: 'string',
format: 'number',
decodedType: 'number',
}),
NonPositive: () =>
E.right({
Expand All @@ -206,6 +212,7 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
maximum: 0,
decodedType: 'number',
}),
NonPositiveInt: () =>
E.right({
Expand All @@ -217,6 +224,7 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
maximum: 0,
decodedType: 'number',
}),
NonZero: () =>
E.right({
Expand All @@ -226,6 +234,7 @@ export const KNOWN_IMPORTS: KnownImports = {
E.right({
type: 'string',
format: 'number',
decodedType: 'number',
}),
NonZeroInt: () =>
E.right({
Expand All @@ -235,6 +244,7 @@ export const KNOWN_IMPORTS: KnownImports = {
E.right({
type: 'string',
format: 'number',
decodedType: 'number',
}),
Positive: () =>
E.right({
Expand All @@ -248,6 +258,7 @@ export const KNOWN_IMPORTS: KnownImports = {
format: 'number',
minimum: 0,
exclusiveMinimum: true,
decodedType: 'number',
}),
Zero: () =>
E.right({
Expand All @@ -257,13 +268,15 @@ export const KNOWN_IMPORTS: KnownImports = {
E.right({
type: 'string',
format: 'number',
decodedType: 'number',
}),
},
'io-ts-bigint': {
BigIntFromString: () =>
E.right({
type: 'string',
format: 'number',
decodedType: 'bigint',
}),
NegativeBigInt: () =>
E.right({
Expand All @@ -275,6 +288,7 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
maximum: -1,
decodedType: 'bigint',
}),
NonEmptyString: () => E.right({ type: 'string', minLength: 1 }),
NonNegativeBigInt: () => E.right({ type: 'number', minimum: 0 }),
Expand All @@ -283,6 +297,7 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
maximum: 0,
decodedType: 'bigint',
}),
NonPositiveBigInt: () =>
E.right({
Expand All @@ -294,28 +309,36 @@ export const KNOWN_IMPORTS: KnownImports = {
type: 'string',
format: 'number',
maximum: 0,
decodedType: 'bigint',
}),
NonZeroBigInt: () => E.right({ type: 'number' }),
NonZeroBigIntFromString: () =>
E.right({
type: 'string',
format: 'number',
decodedType: 'bigint',
}),
PositiveBigInt: () => E.right({ type: 'number', minimum: 1 }),
PositiveBigIntFromString: () =>
E.right({
type: 'string',
format: 'number',
minimum: 1,
decodedType: 'bigint',
}),
ZeroBigInt: () => E.right({ type: 'number' }),
ZeroBigIntFromString: () => E.right({ type: 'string', format: 'number' }),
ZeroBigIntFromString: () =>
E.right({ type: 'string', format: 'number', decodedType: 'bigint' }),
},
'io-ts-types': {
NumberFromString: () => E.right({ type: 'string', format: 'number' }),
BigIntFromString: () => E.right({ type: 'string', format: 'number' }),
BooleanFromNumber: () => E.right({ type: 'number', enum: [0, 1] }),
BooleanFromString: () => E.right({ type: 'string', enum: ['true', 'false'] }),
NumberFromString: () =>
E.right({ type: 'string', format: 'number', decodedType: 'number' }),
BigIntFromString: () =>
E.right({ type: 'string', format: 'number', decodedType: 'bigint' }),
BooleanFromNumber: () =>
E.right({ type: 'number', enum: [0, 1], decodedType: 'boolean' }),
BooleanFromString: () =>
E.right({ type: 'string', enum: ['true', 'false'], decodedType: 'boolean' }),
DateFromISOString: () =>
E.right({ type: 'string', format: 'date-time', title: 'ISO Date String' }),
DateFromNumber: () =>
Expand All @@ -332,7 +355,8 @@ export const KNOWN_IMPORTS: KnownImports = {
format: 'number',
description: 'Number of seconds since the Unix epoch',
}),
IntFromString: () => E.right({ type: 'string', format: 'integer' }),
IntFromString: () =>
E.right({ type: 'string', format: 'integer', decodedType: 'number' }),
JsonFromString: () => E.right({ type: 'string', title: 'JSON String' }),
nonEmptyArray: (_, innerSchema) =>
E.right({ type: 'array', items: innerSchema, minItems: 1 }),
Expand Down
55 changes: 45 additions & 10 deletions packages/openapi-generator/src/optimize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,48 @@ export function foldIntersection(schema: Schema, optimize: OptimizeFn): Schema {
return result;
}

function consolidateUnion(schema: Schema): Schema {
if (schema.type !== 'union') return schema;
if (schema.schemas.length === 1) return schema.schemas[0]!;
if (schema.schemas.length === 0) return { type: 'undefined' };

const consolidatableTypes = ['boolean', 'number', 'string'];
const innerSchemas = schema.schemas.map(optimize);

const isConsolidatableType = (s: Schema): boolean => {
return (
(s.primitive && consolidatableTypes.includes(s.type)) ||
(s.decodedType !== undefined && consolidatableTypes.includes(s.decodedType))
);
};

/**
* We need to check three things:
* 1. All the schemas satisfy isConsolidatableType
* 2. All the schemas have the same decodedType type (aka type at runtime, or the `A` type of the codec)
* 3. At least one of the schemas is a primitive type
*
* If all these conditions are satisfied, we can prove to ourselves that this is a union that
* we can consolidate to the decodedType (runtime) type.
*/

const allConsolidatable = innerSchemas.every(isConsolidatableType);
const hasPrimitive = innerSchemas.some((s: Schema) => s.primitive);

const innerSchemaTypes = new Set(innerSchemas.map((s) => s.decodedType || s.type));
const areSameRuntimeType = innerSchemaTypes.size === 1;

if (allConsolidatable && areSameRuntimeType && hasPrimitive) {
return { type: Array.from(innerSchemaTypes)[0] as Primitive['type'] };
} else {
return schema;
}
}

function mergeUnions(schema: Schema): Schema {
if (schema.type !== 'union') return schema;
else if (schema.schemas.length === 1) return schema.schemas[0]!;
else if (schema.schemas.length === 0) return { type: 'undefined' };
if (schema.schemas.length === 1) return schema.schemas[0]!;
if (schema.schemas.length === 0) return { type: 'undefined' };

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

export function simplifyUnion(schema: Schema, optimize: OptimizeFn): Schema {
if (schema.type !== 'union') {
return schema;
} else if (schema.schemas.length === 1) {
return schema.schemas[0]!;
} else if (schema.schemas.length === 0) {
return { type: 'undefined' };
}
if (schema.type !== 'union') return schema;
if (schema.schemas.length === 1) return schema.schemas[0]!;
if (schema.schemas.length === 0) return { type: 'undefined' };

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

Expand Down Expand Up @@ -176,7 +210,8 @@ export function optimize(schema: Schema): Schema {
}
return newSchema;
} else if (schema.type === 'union') {
const simplified = simplifyUnion(schema, optimize);
const consolidated = consolidateUnion(schema);
const simplified = simplifyUnion(consolidated, optimize);
const merged = mergeUnions(simplified);

if (schema.comment) {
Expand Down
10 changes: 5 additions & 5 deletions packages/openapi-generator/test/apiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ testCase('simple api spec', SIMPLE, '/index.ts', {
path: '/test',
method: 'GET',
parameters: [],
response: { 200: { type: 'string' } },
response: { 200: { type: 'string', primitive: true } },
},
],
});
Expand Down Expand Up @@ -112,7 +112,7 @@ testCase('const route reference', ROUTE_REF, '/index.ts', {
path: '/test',
method: 'GET',
parameters: [],
response: { 200: { type: 'string' } },
response: { 200: { type: 'string', primitive: true } },
},
],
});
Expand Down Expand Up @@ -144,7 +144,7 @@ testCase('const action reference', ACTION_REF, '/index.ts', {
path: '/test',
method: 'GET',
parameters: [],
response: { 200: { type: 'string' } },
response: { 200: { type: 'string', primitive: true } },
},
],
});
Expand Down Expand Up @@ -182,7 +182,7 @@ testCase('spread api spec', SPREAD, '/index.ts', {
path: '/test',
method: 'GET',
parameters: [],
response: { 200: { type: 'string' } },
response: { 200: { type: 'string', primitive: true } },
},
],
});
Expand Down Expand Up @@ -216,7 +216,7 @@ testCase('computed property api spec', COMPUTED_PROPERTY, '/index.ts', {
path: '/test',
method: 'GET',
parameters: [],
response: { 200: { type: 'string' } },
response: { 200: { type: 'string', primitive: true } },
},
],
});
Expand Down
Loading