Skip to content

Commit 2834ccf

Browse files
authored
Merge pull request #1184 from hey-api/feat/parser-plugin-transformers
feat: rewrite date type transform to new parser
2 parents 55d5715 + b1b6f85 commit 2834ccf

File tree

14 files changed

+919
-326
lines changed

14 files changed

+919
-326
lines changed

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

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { IRContext } from '../ir/context';
44
import type { OpenApi } from '../openApi';
55
import { generateSchemas } from '../plugins/@hey-api/schemas/plugin';
66
import { generateServices } from '../plugins/@hey-api/services/plugin';
7+
import { generateTransformers } from '../plugins/@hey-api/transformers/plugin';
78
import { generateTypes } from '../plugins/@hey-api/types/plugin';
89
import type { Client } from '../types/client';
910
import type { Files } from '../types/utils';
@@ -124,38 +125,15 @@ export const generateOutput = async ({ context }: { context: IRContext }) => {
124125
});
125126
}
126127

127-
// types.gen.ts
128+
// TODO: parser - move types, schemas, transformers, and services into plugins
128129
generateTypes({ context });
129-
130-
// schemas.gen.ts
131130
generateSchemas({ context });
132-
133-
// transformers
134-
if (
135-
context.config.services.export &&
136-
// client.services.length &&
137-
context.config.types.dates === 'types+transform'
138-
) {
139-
// await generateLegacyTransformers({
140-
// client,
141-
// onNode: (node) => {
142-
// files.types?.add(node);
143-
// },
144-
// onRemoveNode: () => {
145-
// files.types?.removeNode();
146-
// },
147-
// });
148-
}
149-
150-
// services.gen.ts
131+
generateTransformers({ context });
151132
generateServices({ context });
152133

153-
// TODO: parser - remove after moving types, services, transformers, and schemas into plugin
154-
// index.ts. Any files generated after this won't be included in exports
155-
// from the index file.
134+
// TODO: parser - remove index file after above is migrated to plugins
156135
generateIndexFile({ files: context.files });
157136

158-
// plugins
159137
for (const plugin of context.config.plugins) {
160138
plugin.handler({
161139
context,

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

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
import type { IROperationObject } from './ir';
1+
import type { IROperationObject, IRResponseObject, IRSchemaObject } from './ir';
22
import type { Pagination } from './pagination';
33
import {
44
hasParametersObjectRequired,
55
parameterWithPagination,
66
} from './parameter';
7+
import { deduplicateSchema } from './schema';
8+
import { addItemsToSchema } from './utils';
79

810
export const hasOperationDataRequired = (
911
operation: IROperationObject,
@@ -36,3 +38,145 @@ export const operationPagination = (
3638

3739
return parameterWithPagination(operation.parameters);
3840
};
41+
42+
type StatusGroup = '1XX' | '2XX' | '3XX' | '4XX' | '5XX' | 'default';
43+
44+
const statusCodeToGroup = ({
45+
statusCode,
46+
}: {
47+
statusCode: string;
48+
}): StatusGroup => {
49+
switch (statusCode) {
50+
case '1XX':
51+
return '1XX';
52+
case '2XX':
53+
return '2XX';
54+
case '3XX':
55+
return '3XX';
56+
case '4XX':
57+
return '4XX';
58+
case '5XX':
59+
return '5XX';
60+
case 'default':
61+
return 'default';
62+
default:
63+
return `${statusCode[0]}XX` as StatusGroup;
64+
}
65+
};
66+
67+
interface OperationResponsesMap {
68+
error: IRSchemaObject | undefined;
69+
response: IRSchemaObject | undefined;
70+
}
71+
72+
export const operationResponsesMap = (
73+
operation: IROperationObject,
74+
): OperationResponsesMap => {
75+
const result: OperationResponsesMap = {
76+
error: undefined,
77+
response: undefined,
78+
};
79+
80+
if (!operation.responses) {
81+
return result;
82+
}
83+
84+
let errors: IRSchemaObject = {};
85+
const errorsItems: Array<IRSchemaObject> = [];
86+
87+
let responses: IRSchemaObject = {};
88+
const responsesItems: Array<IRSchemaObject> = [];
89+
90+
let defaultResponse: IRResponseObject | undefined;
91+
92+
for (const name in operation.responses) {
93+
const response = operation.responses[name]!;
94+
95+
switch (statusCodeToGroup({ statusCode: name })) {
96+
case '1XX':
97+
case '3XX':
98+
// TODO: parser - handle informational and redirection status codes
99+
break;
100+
case '2XX':
101+
responsesItems.push(response.schema);
102+
break;
103+
case '4XX':
104+
case '5XX':
105+
errorsItems.push(response.schema);
106+
break;
107+
case 'default':
108+
// store default response to be evaluated last
109+
defaultResponse = response;
110+
break;
111+
}
112+
}
113+
114+
// infer default response type
115+
if (defaultResponse) {
116+
let inferred = false;
117+
118+
// assume default is intended for success if none exists yet
119+
if (!responsesItems.length) {
120+
responsesItems.push(defaultResponse.schema);
121+
inferred = true;
122+
}
123+
124+
const description = (
125+
defaultResponse.schema.description ?? ''
126+
).toLocaleLowerCase();
127+
const $ref = (defaultResponse.schema.$ref ?? '').toLocaleLowerCase();
128+
129+
// TODO: parser - this could be rewritten using regular expressions
130+
const successKeywords = ['success'];
131+
if (
132+
successKeywords.some(
133+
(keyword) => description.includes(keyword) || $ref.includes(keyword),
134+
)
135+
) {
136+
responsesItems.push(defaultResponse.schema);
137+
inferred = true;
138+
}
139+
140+
// TODO: parser - this could be rewritten using regular expressions
141+
const errorKeywords = ['error', 'problem'];
142+
if (
143+
errorKeywords.some(
144+
(keyword) => description.includes(keyword) || $ref.includes(keyword),
145+
)
146+
) {
147+
errorsItems.push(defaultResponse.schema);
148+
inferred = true;
149+
}
150+
151+
// if no keyword match, assume default schema is intended for error
152+
if (!inferred) {
153+
errorsItems.push(defaultResponse.schema);
154+
}
155+
}
156+
157+
if (errorsItems.length) {
158+
errors = addItemsToSchema({
159+
items: errorsItems,
160+
mutateSchemaOneItem: true,
161+
schema: errors,
162+
});
163+
errors = deduplicateSchema({ schema: errors });
164+
if (Object.keys(errors).length) {
165+
result.error = errors;
166+
}
167+
}
168+
169+
if (responsesItems.length) {
170+
responses = addItemsToSchema({
171+
items: responsesItems,
172+
mutateSchemaOneItem: true,
173+
schema: responses,
174+
});
175+
responses = deduplicateSchema({ schema: responses });
176+
if (Object.keys(responses).length) {
177+
result.response = responses;
178+
}
179+
}
180+
181+
return result;
182+
};
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { IRSchemaObject } from './ir';
2+
3+
/**
4+
* Ensure we don't produce redundant types, e.g. string | string.
5+
*/
6+
export const deduplicateSchema = <T extends IRSchemaObject>({
7+
schema,
8+
}: {
9+
schema: T;
10+
}): T => {
11+
if (!schema.items) {
12+
return schema;
13+
}
14+
15+
const uniqueItems: Array<IRSchemaObject> = [];
16+
const typeIds: Array<string> = [];
17+
18+
for (const item of schema.items) {
19+
// skip nested schemas for now, handle if necessary
20+
if (
21+
!item.type ||
22+
item.type === 'boolean' ||
23+
item.type === 'null' ||
24+
item.type === 'number' ||
25+
item.type === 'string' ||
26+
item.type === 'unknown' ||
27+
item.type === 'void'
28+
) {
29+
// const needs namespace to handle empty string values, otherwise
30+
// fallback would equal an actual value and we would skip an item
31+
const typeId = `${item.$ref ?? ''}${item.type ?? ''}${item.const !== undefined ? `const-${item.const}` : ''}`;
32+
if (!typeIds.includes(typeId)) {
33+
typeIds.push(typeId);
34+
uniqueItems.push(item);
35+
}
36+
continue;
37+
}
38+
39+
uniqueItems.push(item);
40+
}
41+
42+
schema.items = uniqueItems;
43+
44+
if (
45+
schema.items.length <= 1 &&
46+
schema.type !== 'array' &&
47+
schema.type !== 'enum' &&
48+
schema.type !== 'tuple'
49+
) {
50+
// bring the only item up to clean up the schema
51+
const liftedSchema = schema.items[0];
52+
delete schema.logicalOperator;
53+
delete schema.items;
54+
schema = {
55+
...schema,
56+
...liftedSchema,
57+
};
58+
}
59+
60+
// exclude unknown if it's the only type left
61+
if (schema.type === 'unknown') {
62+
return {} as T;
63+
}
64+
65+
return schema;
66+
};

packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,11 @@ const schemasV3_1_0 = (context: IRContext<OpenApiV3_1_0>) => {
170170
}
171171
};
172172

173-
export const generateSchemas = async ({
173+
export const generateSchemas = ({
174174
context,
175175
}: {
176176
context: IRContext<ParserOpenApiSpec>;
177-
}): Promise<void> => {
177+
}): void => {
178178
// TODO: parser - once schemas are a plugin, this logic can be simplified
179179
if (!context.config.schemas.export) {
180180
return;

packages/openapi-ts/src/plugins/@hey-api/services/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ const generateFlatServices = ({ context }: { context: IRContext }) => {
420420
}
421421
};
422422

423-
export const generateServices = ({ context }: { context: IRContext }) => {
423+
export const generateServices = ({ context }: { context: IRContext }): void => {
424424
// TODO: parser - once services are a plugin, this logic can be simplified
425425
if (!context.config.services.export) {
426426
return;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { PluginConfig } from './types';
2+
3+
export const defaultConfig: Required<PluginConfig> = {
4+
handler: () => {},
5+
handlerLegacy: () => {},
6+
name: '@hey-api/transformers',
7+
output: 'transformers',
8+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defaultConfig } from './config';
2+
import type { PluginConfig, UserConfig } from './types';
3+
4+
export { defaultConfig } from './config';
5+
export type { PluginConfig, UserConfig } from './types';
6+
7+
/**
8+
* Type helper for Hey API transformers plugin, returns {@link PluginConfig} object
9+
*/
10+
export const defineConfig = (config?: UserConfig): PluginConfig => ({
11+
...defaultConfig,
12+
...config,
13+
});

0 commit comments

Comments
 (0)