Skip to content

Commit 04aa45a

Browse files
committed
fix: support arrow functions that return codecs in OpenAPI generator
When arrow functions that return codecs were imported from a utils module, the OpenAPI generator would output empty schemas ({}) instead of the correct type definitions. Example that broke: ```typescript // utils.ts export const BooleanFromNullableWithFallback = () => fromNullable(t.union([BooleanFromString, t.boolean]), false); // schema.ts import { BooleanFromNullableWithFallback } from './utils'; const Wallet = t.type({ hasLargeNumberOfAddresses: BooleanFromNullableWithFallback() // INCORRECTLY Generates: {} }); ``` The generator couldn't resolve CallExpressions where the callee is an arrow function. When it looked up the identifier and found an arrow function, it had no logic to parse the function body, falling back to an empty schema. This fix: - Adds parseFunctionBody() to extract and parse arrow function return values - Detects when CallExpression callees resolve to arrow functions - Uses findSymbolInitializer() for cross-file lookup of imported functions Test covers the bug scenario: calling an arrow function factory (BooleanFromNullableWithFallback()) within a codec property definition.
1 parent c5000de commit 04aa45a

File tree

3 files changed

+107
-1
lines changed

3 files changed

+107
-1
lines changed

packages/openapi-generator/src/codec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,25 @@ export function parsePlainInitializer(
450450
}
451451
}
452452

453+
function parseFunctionBody(
454+
project: Project,
455+
source: SourceFile,
456+
func: swc.ArrowFunctionExpression,
457+
): E.Either<string, Schema> {
458+
if (func.body === undefined) {
459+
return errorLeft('Function body is undefined');
460+
}
461+
if (func.body.type === 'BlockStatement') {
462+
for (const stmt of func.body.stmts) {
463+
if (stmt.type === 'ReturnStatement' && stmt.argument !== undefined) {
464+
return parseCodecInitializer(project, source, stmt.argument);
465+
}
466+
}
467+
return errorLeft('Function body does not contain a return statement');
468+
}
469+
return parseCodecInitializer(project, source, func.body);
470+
}
471+
453472
export function parseCodecInitializer(
454473
project: Project,
455474
source: SourceFile,
@@ -471,8 +490,29 @@ export function parseCodecInitializer(
471490
} else if (init.type === 'CallExpression') {
472491
const callee = init.callee;
473492
if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') {
474-
return errorLeft(`Unimplemented callee type ${init.callee.type}`);
493+
return errorLeft(`Unimplemented callee type ${callee.type}`);
475494
}
495+
496+
let calleeName: string | [string, string] | undefined;
497+
if (callee.type === 'Identifier') {
498+
calleeName = callee.value;
499+
} else if (
500+
callee.object.type === 'Identifier' &&
501+
callee.property.type === 'Identifier'
502+
) {
503+
calleeName = [callee.object.value, callee.property.value];
504+
}
505+
506+
if (calleeName !== undefined) {
507+
const calleeInitE = findSymbolInitializer(project, source, calleeName);
508+
if (E.isRight(calleeInitE)) {
509+
const [calleeSourceFile, calleeInit] = calleeInitE.right;
510+
if (calleeInit !== null && calleeInit.type === 'ArrowFunctionExpression') {
511+
return parseFunctionBody(project, calleeSourceFile, calleeInit);
512+
}
513+
}
514+
}
515+
476516
const identifierE = codecIdentifier(project, source, callee);
477517
if (E.isLeft(identifierE)) {
478518
return identifierE;

packages/openapi-generator/test/externalModuleApiSpec.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,46 @@ testCase(
368368
},
369369
[],
370370
);
371+
372+
testCase(
373+
'simple api spec with util type functions',
374+
'test/sample-types/apiSpecWithArrow.ts',
375+
{
376+
openapi: '3.0.3',
377+
info: {
378+
title: 'simple api spec with util type functions',
379+
version: '1.0.0',
380+
description: 'simple api spec with util type functions',
381+
},
382+
paths: {
383+
'/test': {
384+
get: {
385+
parameters: [],
386+
responses: {
387+
200: {
388+
description: 'OK',
389+
content: {
390+
'application/json': {
391+
schema: {
392+
type: 'object',
393+
properties: {
394+
hasLargeNumberOfAddresses: {
395+
nullable: true,
396+
type: 'boolean',
397+
},
398+
},
399+
required: ['hasLargeNumberOfAddresses'],
400+
},
401+
},
402+
},
403+
},
404+
},
405+
},
406+
},
407+
},
408+
components: {
409+
schemas: {},
410+
},
411+
},
412+
[],
413+
);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as h from '@api-ts/io-ts-http';
2+
import * as t from 'io-ts';
3+
import { BooleanFromString, fromNullable } from 'io-ts-types';
4+
5+
const BooleanFromNullableWithFallback = () =>
6+
fromNullable(t.union([BooleanFromString, t.boolean]), false);
7+
8+
export const TEST_ROUTE = h.httpRoute({
9+
path: '/test',
10+
method: 'GET',
11+
request: h.httpRequest({}),
12+
response: {
13+
200: t.type({
14+
hasLargeNumberOfAddresses: BooleanFromNullableWithFallback(),
15+
}),
16+
},
17+
});
18+
19+
export const apiSpec = h.apiSpec({
20+
'api.test': {
21+
get: TEST_ROUTE,
22+
},
23+
});

0 commit comments

Comments
 (0)