From cec1e8bdb4ac6a7fa61b26aef836a2ad8edbc9f7 Mon Sep 17 00:00:00 2001 From: Yan Date: Wed, 22 Oct 2025 11:19:52 -0700 Subject: [PATCH] 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. --- packages/openapi-generator/src/codec.ts | 37 +++++++++++++++- .../test/externalModuleApiSpec.test.ts | 43 +++++++++++++++++++ .../test/sample-types/apiSpecWithArrow.ts | 23 ++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 packages/openapi-generator/test/sample-types/apiSpecWithArrow.ts diff --git a/packages/openapi-generator/src/codec.ts b/packages/openapi-generator/src/codec.ts index 175abbd4..e3eb58a4 100644 --- a/packages/openapi-generator/src/codec.ts +++ b/packages/openapi-generator/src/codec.ts @@ -450,6 +450,20 @@ export function parsePlainInitializer( } } +function parseFunctionBody( + project: Project, + source: SourceFile, + func: swc.ArrowFunctionExpression, +): E.Either { + if (func.body === undefined) { + return errorLeft('Function body is undefined'); + } + if (func.body.type === 'BlockStatement') { + return errorLeft('BlockStatement arrow functions are not yet supported'); + } + return parseCodecInitializer(project, source, func.body); +} + export function parseCodecInitializer( project: Project, source: SourceFile, @@ -471,8 +485,29 @@ export function parseCodecInitializer( } else if (init.type === 'CallExpression') { const callee = init.callee; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') { - return errorLeft(`Unimplemented callee type ${init.callee.type}`); + return errorLeft(`Unimplemented callee type ${callee.type}`); + } + + let calleeName: string | [string, string] | undefined; + if (callee.type === 'Identifier') { + calleeName = callee.value; + } else if ( + callee.object.type === 'Identifier' && + callee.property.type === 'Identifier' + ) { + calleeName = [callee.object.value, callee.property.value]; + } + + if (calleeName !== undefined) { + const calleeInitE = findSymbolInitializer(project, source, calleeName); + if (E.isRight(calleeInitE)) { + const [calleeSourceFile, calleeInit] = calleeInitE.right; + if (calleeInit !== null && calleeInit.type === 'ArrowFunctionExpression') { + return parseFunctionBody(project, calleeSourceFile, calleeInit); + } + } } + const identifierE = codecIdentifier(project, source, callee); if (E.isLeft(identifierE)) { return identifierE; diff --git a/packages/openapi-generator/test/externalModuleApiSpec.test.ts b/packages/openapi-generator/test/externalModuleApiSpec.test.ts index 08837ced..7522a169 100644 --- a/packages/openapi-generator/test/externalModuleApiSpec.test.ts +++ b/packages/openapi-generator/test/externalModuleApiSpec.test.ts @@ -368,3 +368,46 @@ testCase( }, [], ); + +testCase( + 'simple api spec with util type functions', + 'test/sample-types/apiSpecWithArrow.ts', + { + openapi: '3.0.3', + info: { + title: 'simple api spec with util type functions', + version: '1.0.0', + description: 'simple api spec with util type functions', + }, + paths: { + '/test': { + get: { + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + hasLargeNumberOfAddresses: { + nullable: true, + type: 'boolean', + }, + }, + required: ['hasLargeNumberOfAddresses'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, + }, + [], +); diff --git a/packages/openapi-generator/test/sample-types/apiSpecWithArrow.ts b/packages/openapi-generator/test/sample-types/apiSpecWithArrow.ts new file mode 100644 index 00000000..edb6a807 --- /dev/null +++ b/packages/openapi-generator/test/sample-types/apiSpecWithArrow.ts @@ -0,0 +1,23 @@ +import * as h from '@api-ts/io-ts-http'; +import * as t from 'io-ts'; +import { BooleanFromString, fromNullable } from 'io-ts-types'; + +const BooleanFromNullableWithFallback = () => + fromNullable(t.union([BooleanFromString, t.boolean]), false); + +export const TEST_ROUTE = h.httpRoute({ + path: '/test', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: t.type({ + hasLargeNumberOfAddresses: BooleanFromNullableWithFallback(), + }), + }, +}); + +export const apiSpec = h.apiSpec({ + 'api.test': { + get: TEST_ROUTE, + }, +});