Skip to content

Commit

Permalink
Merge pull request #1177 from hey-api/refactor/parser-polish
Browse files Browse the repository at this point in the history
refactor: polish parser output
  • Loading branch information
mrlubos authored Oct 21, 2024
2 parents ef44e64 + 92b10e1 commit 11480cc
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 112 deletions.
109 changes: 66 additions & 43 deletions packages/openapi-ts/src/generate/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,16 +834,28 @@ const arrayTypeToIdentifier = ({
);
}

return compiler.typeArrayNode(
schema = deduplicateSchema({ schema });

// at least one item is guaranteed
const itemTypes = schema.items!.map((item) =>
schemaToType({
context,
namespace,
schema: {
...schema,
type: undefined,
},
schema: item,
}),
);

if (itemTypes.length === 1) {
return compiler.typeArrayNode(itemTypes[0]);
}

if (schema.logicalOperator === 'and') {
return compiler.typeArrayNode(
compiler.typeIntersectionNode({ types: itemTypes }),
);
}

return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes }));
};

const booleanTypeToIdentifier = ({
Expand Down Expand Up @@ -1020,10 +1032,13 @@ const objectTypeToIdentifier = ({
type: schemaToType({
context,
namespace,
schema: {
items: indexPropertyItems,
logicalOperator: 'or',
},
schema:
indexPropertyItems.length === 1
? indexPropertyItems[0]
: {
items: indexPropertyItems,
logicalOperator: 'or',
},
}),
};
}
Expand Down Expand Up @@ -1167,11 +1182,11 @@ const schemaTypeToIdentifier = ({
/**
* Ensure we don't produce redundant types, e.g. string | string.
*/
const deduplicateSchema = ({
const deduplicateSchema = <T extends IRSchemaObject>({
schema,
}: {
schema: IRSchemaObject;
}): IRSchemaObject => {
schema: T;
}): T => {
if (!schema.items) {
return schema;
}
Expand All @@ -1182,14 +1197,17 @@ const deduplicateSchema = ({
for (const item of schema.items) {
// skip nested schemas for now, handle if necessary
if (
!item.type ||
item.type === 'boolean' ||
item.type === 'null' ||
item.type === 'number' ||
item.type === 'string' ||
item.type === 'unknown' ||
item.type === 'void'
) {
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const ?? ''}`;
// const needs namespace to handle empty string values, otherwise
// fallback would equal an actual value and we would skip an item
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`;
if (!typeIds.includes(typeId)) {
typeIds.push(typeId);
uniqueItems.push(item);
Expand Down Expand Up @@ -1220,7 +1238,7 @@ const deduplicateSchema = ({

// exclude unknown if it's the only type left
if (schema.type === 'unknown') {
return {};
return {} as T;
}

return schema;
Expand Down Expand Up @@ -1379,10 +1397,10 @@ const operationToResponseTypes = ({
return;
}

const errors: IRSchemaObject = {};
let errors: IRSchemaObject = {};
const errorsItems: Array<IRSchemaObject> = [];

const responses: IRSchemaObject = {};
let responses: IRSchemaObject = {};
const responsesItems: Array<IRSchemaObject> = [];

let defaultResponse: IRResponseObject | undefined;
Expand Down Expand Up @@ -1452,21 +1470,14 @@ const operationToResponseTypes = ({
}
}

addItemsToSchema({
items: errorsItems,
schema: errors,
});

addItemsToSchema({
items: responsesItems,
schema: responses,
});

if (errors.items) {
const deduplicatedSchema = deduplicateSchema({
if (errorsItems.length) {
errors = addItemsToSchema({
items: errorsItems,
mutateSchemaOneItem: true,
schema: errors,
});
if (Object.keys(deduplicatedSchema).length) {
errors = deduplicateSchema({ schema: errors });
if (Object.keys(errors).length) {
const identifier = context.file({ id: typesId })!.identifier({
$ref: operationErrorRef({ id: operation.id }),
create: true,
Expand All @@ -1477,18 +1488,21 @@ const operationToResponseTypes = ({
name: identifier.name,
type: schemaToType({
context,
schema: deduplicatedSchema,
schema: errors,
}),
});
context.file({ id: typesId })!.add(node);
}
}

if (responses.items) {
const deduplicatedSchema = deduplicateSchema({
if (responsesItems.length) {
responses = addItemsToSchema({
items: responsesItems,
mutateSchemaOneItem: true,
schema: responses,
});
if (Object.keys(deduplicatedSchema).length) {
responses = deduplicateSchema({ schema: responses });
if (Object.keys(responses).length) {
const identifier = context.file({ id: typesId })!.identifier({
$ref: operationResponseRef({ id: operation.id }),
create: true,
Expand All @@ -1499,7 +1513,7 @@ const operationToResponseTypes = ({
name: identifier.name,
type: schemaToType({
context,
schema: deduplicatedSchema,
schema: responses,
}),
});
context.file({ id: typesId })!.add(node);
Expand Down Expand Up @@ -1555,17 +1569,26 @@ const schemaToType = ({
schema,
});
} else if (schema.items) {
const itemTypes = schema.items.map((item) =>
schemaToType({
schema = deduplicateSchema({ schema });
if (schema.items) {
const itemTypes = schema.items.map((item) =>
schemaToType({
context,
namespace,
schema: item,
}),
);
type =
schema.logicalOperator === 'and'
? compiler.typeIntersectionNode({ types: itemTypes })
: compiler.typeUnionNode({ types: itemTypes });
} else {
type = schemaToType({
context,
namespace,
schema: item,
}),
);
type =
schema.logicalOperator === 'and'
? compiler.typeIntersectionNode({ types: itemTypes })
: compiler.typeUnionNode({ types: itemTypes });
schema,
});
}
} else {
// catch-all fallback for failed schemas
type = schemaTypeToIdentifier({
Expand Down
29 changes: 24 additions & 5 deletions packages/openapi-ts/src/ir/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,39 @@ import type { IRSchemaObject } from './ir';
*/
export const addItemsToSchema = ({
items,
logicalOperator = 'or',
mutateSchemaOneItem = false,
schema,
}: {
items: Array<IRSchemaObject>;
logicalOperator?: IRSchemaObject['logicalOperator'];
mutateSchemaOneItem?: boolean;
schema: IRSchemaObject;
}) => {
if (!items.length) {
return;
return schema;
}

schema.items = items;
if (schema.type === 'tuple') {
schema.items = items;
return schema;
}

if (items.length === 1 || schema.type === 'tuple') {
return;
if (items.length !== 1) {
schema.items = items;
schema.logicalOperator = logicalOperator;
return schema;
}

schema.logicalOperator = 'or';
if (mutateSchemaOneItem) {
// bring composition up to avoid extraneous brackets
schema = {
...schema,
...items[0],
};
return schema;
}

schema.items = items;
return schema;
};
24 changes: 14 additions & 10 deletions packages/openapi-ts/src/openApi/3.1.0/parser/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const parseArray = ({
}
}

addItemsToSchema({
irSchema = addItemsToSchema({
items: schemaItems,
schema: irSchema,
});
Expand Down Expand Up @@ -348,10 +348,12 @@ const parseAllOf = ({
}
}

if (schemaItems.length) {
irSchema.items = schemaItems;
irSchema.logicalOperator = 'and';
}
irSchema = addItemsToSchema({
items: schemaItems,
logicalOperator: 'and',
mutateSchemaOneItem: true,
schema: irSchema,
});

if (schemaTypes.includes('null')) {
// nest composition to avoid producing an intersection with null
Expand Down Expand Up @@ -404,8 +406,9 @@ const parseAnyOf = ({
schemaItems.push({ type: 'null' });
}

addItemsToSchema({
irSchema = addItemsToSchema({
items: schemaItems,
mutateSchemaOneItem: true,
schema: irSchema,
});

Expand Down Expand Up @@ -442,7 +445,7 @@ const parseEnum = ({
context: IRContext;
schema: SchemaWithRequired<'enum'>;
}): IRSchemaObject => {
const irSchema = initIrSchema({ schema });
let irSchema = initIrSchema({ schema });

irSchema.type = 'enum';

Expand Down Expand Up @@ -477,7 +480,7 @@ const parseEnum = ({
}
}

addItemsToSchema({
irSchema = addItemsToSchema({
items: schemaItems,
schema: irSchema,
});
Expand Down Expand Up @@ -517,8 +520,9 @@ const parseOneOf = ({
schemaItems.push({ type: 'null' });
}

addItemsToSchema({
irSchema = addItemsToSchema({
items: schemaItems,
mutateSchemaOneItem: true,
schema: irSchema,
});

Expand Down Expand Up @@ -659,7 +663,7 @@ const parseManyTypes = ({
);
}

addItemsToSchema({
irSchema = addItemsToSchema({
items: schemaItems,
schema: irSchema,
});
Expand Down
23 changes: 23 additions & 0 deletions packages/openapi-ts/test/3.1.0.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const outputDir = path.join(__dirname, 'generated', VERSION);
describe(`OpenAPI ${VERSION}`, () => {
const createConfig = (userConfig: UserConfig): UserConfig => ({
client: '@hey-api/client-fetch',
experimental_parser: true,
schemas: false,
...userConfig,
input: path.join(
Expand All @@ -43,6 +44,17 @@ describe(`OpenAPI ${VERSION}`, () => {
}),
description: 'does not generate duplicate null',
},
{
config: createConfig({
input: 'object-properties-all-of.json',
output: 'object-properties-all-of',
services: {
export: false,
},
}),
description:
'sets correct logical operator and brackets on object with properties and allOf composition',
},
{
config: createConfig({
input: 'object-properties-any-of.json',
Expand All @@ -54,6 +66,17 @@ describe(`OpenAPI ${VERSION}`, () => {
description:
'sets correct logical operator and brackets on object with properties and anyOf composition',
},
{
config: createConfig({
input: 'object-properties-one-of.json',
output: 'object-properties-one-of',
services: {
export: false,
},
}),
description:
'sets correct logical operator and brackets on object with properties and oneOf composition',
},
{
config: createConfig({
input: 'required-all-of-ref.json',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
// This file is auto-generated by @hey-api/openapi-ts

export type PostTestData = {
/**
* should not produce duplicate null
*/
body?: {
weirdEnum?: ('' | (string) | null);
};
};

export type $OpenApiTs = {
'/test': {
post: {
req: PostTestData;
};
};
};
/**
* should not produce duplicate null
*/
export type WeirdEnum = '' | string | null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
Loading

0 comments on commit 11480cc

Please sign in to comment.