Skip to content

Commit 11480cc

Browse files
authored
Merge pull request #1177 from hey-api/refactor/parser-polish
refactor: polish parser output
2 parents ef44e64 + 92b10e1 commit 11480cc

File tree

17 files changed

+237
-112
lines changed

17 files changed

+237
-112
lines changed

packages/openapi-ts/src/generate/types.ts

Lines changed: 66 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -834,16 +834,28 @@ const arrayTypeToIdentifier = ({
834834
);
835835
}
836836

837-
return compiler.typeArrayNode(
837+
schema = deduplicateSchema({ schema });
838+
839+
// at least one item is guaranteed
840+
const itemTypes = schema.items!.map((item) =>
838841
schemaToType({
839842
context,
840843
namespace,
841-
schema: {
842-
...schema,
843-
type: undefined,
844-
},
844+
schema: item,
845845
}),
846846
);
847+
848+
if (itemTypes.length === 1) {
849+
return compiler.typeArrayNode(itemTypes[0]);
850+
}
851+
852+
if (schema.logicalOperator === 'and') {
853+
return compiler.typeArrayNode(
854+
compiler.typeIntersectionNode({ types: itemTypes }),
855+
);
856+
}
857+
858+
return compiler.typeArrayNode(compiler.typeUnionNode({ types: itemTypes }));
847859
};
848860

849861
const booleanTypeToIdentifier = ({
@@ -1020,10 +1032,13 @@ const objectTypeToIdentifier = ({
10201032
type: schemaToType({
10211033
context,
10221034
namespace,
1023-
schema: {
1024-
items: indexPropertyItems,
1025-
logicalOperator: 'or',
1026-
},
1035+
schema:
1036+
indexPropertyItems.length === 1
1037+
? indexPropertyItems[0]
1038+
: {
1039+
items: indexPropertyItems,
1040+
logicalOperator: 'or',
1041+
},
10271042
}),
10281043
};
10291044
}
@@ -1167,11 +1182,11 @@ const schemaTypeToIdentifier = ({
11671182
/**
11681183
* Ensure we don't produce redundant types, e.g. string | string.
11691184
*/
1170-
const deduplicateSchema = ({
1185+
const deduplicateSchema = <T extends IRSchemaObject>({
11711186
schema,
11721187
}: {
1173-
schema: IRSchemaObject;
1174-
}): IRSchemaObject => {
1188+
schema: T;
1189+
}): T => {
11751190
if (!schema.items) {
11761191
return schema;
11771192
}
@@ -1182,14 +1197,17 @@ const deduplicateSchema = ({
11821197
for (const item of schema.items) {
11831198
// skip nested schemas for now, handle if necessary
11841199
if (
1200+
!item.type ||
11851201
item.type === 'boolean' ||
11861202
item.type === 'null' ||
11871203
item.type === 'number' ||
11881204
item.type === 'string' ||
11891205
item.type === 'unknown' ||
11901206
item.type === 'void'
11911207
) {
1192-
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const ?? ''}`;
1208+
// const needs namespace to handle empty string values, otherwise
1209+
// fallback would equal an actual value and we would skip an item
1210+
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`;
11931211
if (!typeIds.includes(typeId)) {
11941212
typeIds.push(typeId);
11951213
uniqueItems.push(item);
@@ -1220,7 +1238,7 @@ const deduplicateSchema = ({
12201238

12211239
// exclude unknown if it's the only type left
12221240
if (schema.type === 'unknown') {
1223-
return {};
1241+
return {} as T;
12241242
}
12251243

12261244
return schema;
@@ -1379,10 +1397,10 @@ const operationToResponseTypes = ({
13791397
return;
13801398
}
13811399

1382-
const errors: IRSchemaObject = {};
1400+
let errors: IRSchemaObject = {};
13831401
const errorsItems: Array<IRSchemaObject> = [];
13841402

1385-
const responses: IRSchemaObject = {};
1403+
let responses: IRSchemaObject = {};
13861404
const responsesItems: Array<IRSchemaObject> = [];
13871405

13881406
let defaultResponse: IRResponseObject | undefined;
@@ -1452,21 +1470,14 @@ const operationToResponseTypes = ({
14521470
}
14531471
}
14541472

1455-
addItemsToSchema({
1456-
items: errorsItems,
1457-
schema: errors,
1458-
});
1459-
1460-
addItemsToSchema({
1461-
items: responsesItems,
1462-
schema: responses,
1463-
});
1464-
1465-
if (errors.items) {
1466-
const deduplicatedSchema = deduplicateSchema({
1473+
if (errorsItems.length) {
1474+
errors = addItemsToSchema({
1475+
items: errorsItems,
1476+
mutateSchemaOneItem: true,
14671477
schema: errors,
14681478
});
1469-
if (Object.keys(deduplicatedSchema).length) {
1479+
errors = deduplicateSchema({ schema: errors });
1480+
if (Object.keys(errors).length) {
14701481
const identifier = context.file({ id: typesId })!.identifier({
14711482
$ref: operationErrorRef({ id: operation.id }),
14721483
create: true,
@@ -1477,18 +1488,21 @@ const operationToResponseTypes = ({
14771488
name: identifier.name,
14781489
type: schemaToType({
14791490
context,
1480-
schema: deduplicatedSchema,
1491+
schema: errors,
14811492
}),
14821493
});
14831494
context.file({ id: typesId })!.add(node);
14841495
}
14851496
}
14861497

1487-
if (responses.items) {
1488-
const deduplicatedSchema = deduplicateSchema({
1498+
if (responsesItems.length) {
1499+
responses = addItemsToSchema({
1500+
items: responsesItems,
1501+
mutateSchemaOneItem: true,
14891502
schema: responses,
14901503
});
1491-
if (Object.keys(deduplicatedSchema).length) {
1504+
responses = deduplicateSchema({ schema: responses });
1505+
if (Object.keys(responses).length) {
14921506
const identifier = context.file({ id: typesId })!.identifier({
14931507
$ref: operationResponseRef({ id: operation.id }),
14941508
create: true,
@@ -1499,7 +1513,7 @@ const operationToResponseTypes = ({
14991513
name: identifier.name,
15001514
type: schemaToType({
15011515
context,
1502-
schema: deduplicatedSchema,
1516+
schema: responses,
15031517
}),
15041518
});
15051519
context.file({ id: typesId })!.add(node);
@@ -1555,17 +1569,26 @@ const schemaToType = ({
15551569
schema,
15561570
});
15571571
} else if (schema.items) {
1558-
const itemTypes = schema.items.map((item) =>
1559-
schemaToType({
1572+
schema = deduplicateSchema({ schema });
1573+
if (schema.items) {
1574+
const itemTypes = schema.items.map((item) =>
1575+
schemaToType({
1576+
context,
1577+
namespace,
1578+
schema: item,
1579+
}),
1580+
);
1581+
type =
1582+
schema.logicalOperator === 'and'
1583+
? compiler.typeIntersectionNode({ types: itemTypes })
1584+
: compiler.typeUnionNode({ types: itemTypes });
1585+
} else {
1586+
type = schemaToType({
15601587
context,
15611588
namespace,
1562-
schema: item,
1563-
}),
1564-
);
1565-
type =
1566-
schema.logicalOperator === 'and'
1567-
? compiler.typeIntersectionNode({ types: itemTypes })
1568-
: compiler.typeUnionNode({ types: itemTypes });
1589+
schema,
1590+
});
1591+
}
15691592
} else {
15701593
// catch-all fallback for failed schemas
15711594
type = schemaTypeToIdentifier({

packages/openapi-ts/src/ir/utils.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,39 @@ import type { IRSchemaObject } from './ir';
66
*/
77
export const addItemsToSchema = ({
88
items,
9+
logicalOperator = 'or',
10+
mutateSchemaOneItem = false,
911
schema,
1012
}: {
1113
items: Array<IRSchemaObject>;
14+
logicalOperator?: IRSchemaObject['logicalOperator'];
15+
mutateSchemaOneItem?: boolean;
1216
schema: IRSchemaObject;
1317
}) => {
1418
if (!items.length) {
15-
return;
19+
return schema;
1620
}
1721

18-
schema.items = items;
22+
if (schema.type === 'tuple') {
23+
schema.items = items;
24+
return schema;
25+
}
1926

20-
if (items.length === 1 || schema.type === 'tuple') {
21-
return;
27+
if (items.length !== 1) {
28+
schema.items = items;
29+
schema.logicalOperator = logicalOperator;
30+
return schema;
2231
}
2332

24-
schema.logicalOperator = 'or';
33+
if (mutateSchemaOneItem) {
34+
// bring composition up to avoid extraneous brackets
35+
schema = {
36+
...schema,
37+
...items[0],
38+
};
39+
return schema;
40+
}
41+
42+
schema.items = items;
43+
return schema;
2544
};

packages/openapi-ts/src/openApi/3.1.0/parser/schema.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const parseArray = ({
135135
}
136136
}
137137

138-
addItemsToSchema({
138+
irSchema = addItemsToSchema({
139139
items: schemaItems,
140140
schema: irSchema,
141141
});
@@ -348,10 +348,12 @@ const parseAllOf = ({
348348
}
349349
}
350350

351-
if (schemaItems.length) {
352-
irSchema.items = schemaItems;
353-
irSchema.logicalOperator = 'and';
354-
}
351+
irSchema = addItemsToSchema({
352+
items: schemaItems,
353+
logicalOperator: 'and',
354+
mutateSchemaOneItem: true,
355+
schema: irSchema,
356+
});
355357

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

407-
addItemsToSchema({
409+
irSchema = addItemsToSchema({
408410
items: schemaItems,
411+
mutateSchemaOneItem: true,
409412
schema: irSchema,
410413
});
411414

@@ -442,7 +445,7 @@ const parseEnum = ({
442445
context: IRContext;
443446
schema: SchemaWithRequired<'enum'>;
444447
}): IRSchemaObject => {
445-
const irSchema = initIrSchema({ schema });
448+
let irSchema = initIrSchema({ schema });
446449

447450
irSchema.type = 'enum';
448451

@@ -477,7 +480,7 @@ const parseEnum = ({
477480
}
478481
}
479482

480-
addItemsToSchema({
483+
irSchema = addItemsToSchema({
481484
items: schemaItems,
482485
schema: irSchema,
483486
});
@@ -517,8 +520,9 @@ const parseOneOf = ({
517520
schemaItems.push({ type: 'null' });
518521
}
519522

520-
addItemsToSchema({
523+
irSchema = addItemsToSchema({
521524
items: schemaItems,
525+
mutateSchemaOneItem: true,
522526
schema: irSchema,
523527
});
524528

@@ -659,7 +663,7 @@ const parseManyTypes = ({
659663
);
660664
}
661665

662-
addItemsToSchema({
666+
irSchema = addItemsToSchema({
663667
items: schemaItems,
664668
schema: irSchema,
665669
});

packages/openapi-ts/test/3.1.0.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const outputDir = path.join(__dirname, 'generated', VERSION);
1818
describe(`OpenAPI ${VERSION}`, () => {
1919
const createConfig = (userConfig: UserConfig): UserConfig => ({
2020
client: '@hey-api/client-fetch',
21+
experimental_parser: true,
2122
schemas: false,
2223
...userConfig,
2324
input: path.join(
@@ -43,6 +44,17 @@ describe(`OpenAPI ${VERSION}`, () => {
4344
}),
4445
description: 'does not generate duplicate null',
4546
},
47+
{
48+
config: createConfig({
49+
input: 'object-properties-all-of.json',
50+
output: 'object-properties-all-of',
51+
services: {
52+
export: false,
53+
},
54+
}),
55+
description:
56+
'sets correct logical operator and brackets on object with properties and allOf composition',
57+
},
4658
{
4759
config: createConfig({
4860
input: 'object-properties-any-of.json',
@@ -54,6 +66,17 @@ describe(`OpenAPI ${VERSION}`, () => {
5466
description:
5567
'sets correct logical operator and brackets on object with properties and anyOf composition',
5668
},
69+
{
70+
config: createConfig({
71+
input: 'object-properties-one-of.json',
72+
output: 'object-properties-one-of',
73+
services: {
74+
export: false,
75+
},
76+
}),
77+
description:
78+
'sets correct logical operator and brackets on object with properties and oneOf composition',
79+
},
5780
{
5881
config: createConfig({
5982
input: 'required-all-of-ref.json',
Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,6 @@
11
// This file is auto-generated by @hey-api/openapi-ts
22

3-
export type PostTestData = {
4-
/**
5-
* should not produce duplicate null
6-
*/
7-
body?: {
8-
weirdEnum?: ('' | (string) | null);
9-
};
10-
};
11-
12-
export type $OpenApiTs = {
13-
'/test': {
14-
post: {
15-
req: PostTestData;
16-
};
17-
};
18-
};
3+
/**
4+
* should not produce duplicate null
5+
*/
6+
export type WeirdEnum = '' | string | null;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// This file is auto-generated by @hey-api/openapi-ts
2+
export * from './types.gen';

0 commit comments

Comments
 (0)