Skip to content

Commit 37bd4ed

Browse files
committed
chore: draft implementation
1 parent d116eac commit 37bd4ed

4 files changed

Lines changed: 455 additions & 7 deletions

File tree

packages/openapi-ts/src/plugins/@hey-api/examples/config.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { definePluginConfig } from '@hey-api/shared';
22

3-
// import { handler } from './plugin';
3+
import { handler } from './plugin';
44
import type { HeyApiExamplesPlugin } from './types';
55

66
export const defaultConfig: HeyApiExamplesPlugin['Config'] = {
77
config: {
88
case: 'camelCase',
99
includeInEntry: false,
1010
},
11-
// handler,
12-
handler: () => {},
11+
handler,
1312
name: '@hey-api/examples',
13+
resolveConfig: (plugin, context) => {
14+
plugin.config.case = context.valueToObject({
15+
defaultValue: 'camelCase' as const,
16+
value: plugin.config.case,
17+
});
18+
},
1419
tags: ['source'],
1520
};
1621

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
import { toCase } from '@hey-api/shared';
2+
3+
import { $ } from '../../../ts-dsl';
4+
import type { HeyApiExamplesPlugin } from './types';
5+
6+
export const handler: HeyApiExamplesPlugin['Handler'] = ({ plugin }) => {
7+
const spec = plugin.context.spec as {
8+
components?: {
9+
examples?: Record<string, { value?: unknown }>;
10+
schemas?: Record<string, { example?: unknown; examples?: Record<string, unknown> }>;
11+
};
12+
paths?: Record<
13+
string,
14+
Record<
15+
string,
16+
{
17+
operationId?: string;
18+
responses?: Record<
19+
string,
20+
{
21+
content?: Record<string, { example?: unknown; examples?: Record<string, unknown> }>;
22+
}
23+
>;
24+
}
25+
>
26+
>;
27+
};
28+
29+
if (!spec.components?.schemas) {
30+
return;
31+
}
32+
33+
const schemas = spec.components.schemas;
34+
const examplesComponent = spec.components.examples;
35+
36+
for (const [name, schema] of Object.entries(schemas)) {
37+
const schemaExample = resolveSchemaExample(schema, examplesComponent);
38+
if (!schemaExample) {
39+
continue;
40+
}
41+
42+
const hasPluralExamples = schema.examples && Object.keys(schema.examples).length > 0;
43+
const functionName = `${toCase(name, 'camelCase')}Example`;
44+
45+
if (hasPluralExamples) {
46+
generatePluralSchemaFactory({
47+
functionName,
48+
name,
49+
plugin,
50+
schemaExample: schemaExample as Record<string, unknown>,
51+
});
52+
} else {
53+
generateSingularSchemaFactory({
54+
functionName,
55+
name,
56+
plugin,
57+
schemaExample,
58+
});
59+
}
60+
}
61+
62+
if (!spec.paths) {
63+
return;
64+
}
65+
66+
for (const [, pathItem] of Object.entries(spec.paths)) {
67+
for (const [, operation] of Object.entries(pathItem)) {
68+
if ('parameters' in operation || 'summary' in operation || 'description' in operation) {
69+
continue;
70+
}
71+
72+
if (!('responses' in operation) || !operation.responses) {
73+
continue;
74+
}
75+
76+
const operationExamples = collectOperationExamples(
77+
operation.responses as Record<
78+
string,
79+
{
80+
content?: Record<string, { example?: unknown; examples?: Record<string, unknown> }>;
81+
}
82+
>,
83+
examplesComponent,
84+
);
85+
if (operationExamples.statusCodes.size === 0) {
86+
continue;
87+
}
88+
89+
generateOperationFactory({
90+
examples: operationExamples,
91+
functionName: `${toCase(operation.operationId!, 'camelCase')}Example`,
92+
plugin,
93+
});
94+
}
95+
}
96+
};
97+
98+
interface ResolvedExample {
99+
example?: unknown;
100+
examples?: Record<string, unknown>;
101+
}
102+
103+
function resolveSchemaExample(
104+
schema: ResolvedExample,
105+
examplesComponent?: Record<string, { value?: unknown }>,
106+
): unknown | Record<string, unknown> | null {
107+
if (schema.example !== undefined) {
108+
if (typeof schema.example === 'object' && schema.example !== null && '$ref' in schema.example) {
109+
return resolveRefExample(schema.example.$ref as string, examplesComponent);
110+
}
111+
return schema.example;
112+
}
113+
114+
if (schema.examples && Object.keys(schema.examples).length > 0) {
115+
const resolved: Record<string, unknown> = {};
116+
for (const [key, exampleRef] of Object.entries(schema.examples)) {
117+
if (typeof exampleRef === 'object' && exampleRef !== null && '$ref' in exampleRef) {
118+
const resolvedValue = resolveRefExample(exampleRef.$ref as string, examplesComponent);
119+
if (resolvedValue !== undefined) {
120+
resolved[key] = resolvedValue;
121+
}
122+
} else if (typeof exampleRef === 'object' && exampleRef !== null && 'value' in exampleRef) {
123+
resolved[key] = exampleRef.value;
124+
} else {
125+
resolved[key] = exampleRef;
126+
}
127+
}
128+
return resolved;
129+
}
130+
131+
return null;
132+
}
133+
134+
function resolveRefExample($ref: string, examples?: Record<string, { value?: unknown }>): unknown {
135+
if (!examples) {
136+
return undefined;
137+
}
138+
139+
const refPath = $ref.split('/');
140+
const exampleName = refPath[refPath.length - 1]!;
141+
const example = examples[exampleName];
142+
143+
if (!example) {
144+
return undefined;
145+
}
146+
147+
return example.value;
148+
}
149+
150+
function getJsonExample(
151+
content?: Record<string, { example?: unknown; examples?: Record<string, unknown> }>,
152+
): unknown | Record<string, unknown> | null {
153+
if (!content) {
154+
return null;
155+
}
156+
157+
const jsonContent = content['application/json'];
158+
if (!jsonContent) {
159+
return null;
160+
}
161+
162+
if (jsonContent.example !== undefined) {
163+
return jsonContent.example;
164+
}
165+
166+
if (jsonContent.examples && Object.keys(jsonContent.examples).length > 0) {
167+
const resolved: Record<string, unknown> = {};
168+
for (const [key, exampleRef] of Object.entries(jsonContent.examples)) {
169+
if (typeof exampleRef === 'object' && exampleRef !== null && 'value' in exampleRef) {
170+
resolved[key] = exampleRef.value;
171+
} else {
172+
resolved[key] = exampleRef;
173+
}
174+
}
175+
return resolved;
176+
}
177+
178+
return null;
179+
}
180+
181+
interface OperationExamples {
182+
defaultStatusCode?: string;
183+
statusCodes: Map<
184+
string,
185+
{
186+
examples: Record<string, unknown>;
187+
isPlural: boolean;
188+
}
189+
>;
190+
}
191+
192+
function collectOperationExamples(
193+
responses: Record<
194+
string,
195+
{
196+
content?: Record<string, { example?: unknown; examples?: Record<string, unknown> }>;
197+
}
198+
>,
199+
examplesComponent?: Record<string, { value?: unknown }>,
200+
): OperationExamples {
201+
const result: OperationExamples = {
202+
statusCodes: new Map(),
203+
};
204+
205+
for (const [statusCode, response] of Object.entries(responses)) {
206+
const jsonExample = getJsonExample(response.content);
207+
if (!jsonExample) {
208+
continue;
209+
}
210+
211+
const isPlural =
212+
typeof jsonExample === 'object' && jsonExample !== null && !Array.isArray(jsonExample);
213+
214+
let resolvedExamples: Record<string, unknown>;
215+
216+
if (isPlural && typeof jsonExample === 'object') {
217+
const resolved: Record<string, unknown> = {};
218+
for (const [key, value] of Object.entries(jsonExample)) {
219+
if (typeof value === 'object' && value !== null && '$ref' in value) {
220+
const refResolved = resolveRefExample(value.$ref as string, examplesComponent);
221+
if (refResolved !== undefined) {
222+
resolved[key] = refResolved;
223+
}
224+
} else {
225+
resolved[key] = value;
226+
}
227+
}
228+
resolvedExamples = resolved;
229+
} else {
230+
resolvedExamples = { basic: jsonExample as unknown };
231+
}
232+
233+
if (Object.keys(resolvedExamples).length === 0) {
234+
continue;
235+
}
236+
237+
result.statusCodes.set(statusCode, {
238+
examples: resolvedExamples,
239+
isPlural,
240+
});
241+
242+
if (!result.defaultStatusCode) {
243+
const codeNum = parseInt(statusCode.replace('X', '0').replace('default', '999'));
244+
const defaultNum = result.defaultStatusCode
245+
? parseInt(result.defaultStatusCode.replace('X', '0').replace('default', '999'))
246+
: 999;
247+
248+
if (statusCode.startsWith('2') && !result.defaultStatusCode) {
249+
result.defaultStatusCode = statusCode;
250+
} else if (codeNum < defaultNum && statusCode !== 'default') {
251+
result.defaultStatusCode = statusCode;
252+
}
253+
}
254+
}
255+
256+
if (!result.defaultStatusCode && result.statusCodes.size > 0) {
257+
result.defaultStatusCode = Array.from(result.statusCodes.keys())[0]!;
258+
}
259+
260+
return result;
261+
}
262+
263+
function generateSingularSchemaFactory({
264+
functionName,
265+
name,
266+
plugin,
267+
schemaExample,
268+
}: {
269+
functionName: string;
270+
name: string;
271+
plugin: HeyApiExamplesPlugin['Instance'];
272+
schemaExample: unknown;
273+
}): void {
274+
const symbol = plugin.symbol(functionName, {
275+
meta: {
276+
category: 'example',
277+
resource: 'schema',
278+
resourceId: name,
279+
},
280+
});
281+
282+
const func = $.func(symbol).decl();
283+
// @ts-expect-error TODO
284+
func.$do($.return($.fromValue(schemaExample, { layout: 'pretty' })));
285+
286+
plugin.node(func);
287+
}
288+
289+
function generatePluralSchemaFactory({
290+
functionName,
291+
name,
292+
plugin,
293+
schemaExample,
294+
}: {
295+
functionName: string;
296+
name: string;
297+
plugin: HeyApiExamplesPlugin['Instance'];
298+
schemaExample: Record<string, unknown>;
299+
}): void {
300+
const exampleKeys = Object.keys(schemaExample);
301+
const firstKey = exampleKeys[0]!;
302+
303+
const symbol = plugin.symbol(functionName, {
304+
meta: {
305+
category: 'example',
306+
resource: 'schema',
307+
resourceId: name,
308+
},
309+
});
310+
311+
const func = $.func(symbol).decl();
312+
// @ts-expect-error TODO
313+
func.param($.param('options'));
314+
315+
// @ts-expect-error TODO
316+
func.$do($.return($.fromValue(schemaExample[firstKey]!, { layout: 'pretty' })));
317+
318+
plugin.node(func);
319+
}
320+
321+
function generateOperationFactory({
322+
examples: operationExamples,
323+
functionName,
324+
plugin,
325+
}: {
326+
examples: OperationExamples;
327+
functionName: string;
328+
plugin: HeyApiExamplesPlugin['Instance'];
329+
}): void {
330+
const statusCodes = Array.from(operationExamples.statusCodes.entries());
331+
332+
const defaultStatusCode = operationExamples.defaultStatusCode || statusCodes[0]?.[0];
333+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
334+
const defaultCodeNum = defaultStatusCode ? parseInt(defaultStatusCode.replace('X', '0')) : 200;
335+
336+
const symbol = plugin.symbol(functionName, {
337+
meta: {
338+
category: 'example',
339+
resource: 'operation',
340+
resourceId: functionName.replace('Example', ''),
341+
},
342+
});
343+
344+
const func = $.func(symbol).decl();
345+
// @ts-expect-error TODO
346+
func.param($.param('options'));
347+
348+
// @ts-expect-error TODO
349+
func.$do(
350+
$.return(
351+
$.fromValue(
352+
statusCodes[0]?.[1]?.examples.basic ??
353+
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
354+
statusCodes[0]?.[1]?.examples[Object.keys(statusCodes[0]?.[1]?.examples ?? {})[0]!]!,
355+
{ layout: 'pretty' },
356+
),
357+
),
358+
);
359+
360+
plugin.node(func);
361+
}

0 commit comments

Comments
 (0)