Skip to content

Commit 61b5acf

Browse files
feat(openapi-generator): support Identifier type in route expressions
Add support for passing variables directly to apiSpec() without requiring object literal syntax with spread operator. Previously, this would fail: ```typescript export const MyApiSpec = apiSpec(myApiSpecProps); ``` And required this workaround: ```typescript export const MyApiSpec = apiSpec({ ...myApiSpecProps }); ``` This change resolves identifiers when passed directly to apiSpec(), handling both nested and imported identifiers. Closes Ticket: DX-1604
1 parent c75b51b commit 61b5acf

File tree

2 files changed

+214
-0
lines changed

2 files changed

+214
-0
lines changed

packages/openapi-generator/src/apiSpec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export function parseApiSpec(
1515
sourceFile: SourceFile,
1616
expr: swc.Expression,
1717
): E.Either<string, Route[]> {
18+
// If apiSpec is passed an identifier (variable), first resolve it to its actual value
19+
if (expr.type === 'Identifier') {
20+
const resolvedE = resolveLiteralOrIdentifier(project, sourceFile, expr);
21+
if (E.isLeft(resolvedE)) {
22+
return resolvedE;
23+
}
24+
const [newSourceFile, resolvedExpr] = resolvedE.right;
25+
return parseApiSpec(project, newSourceFile, resolvedExpr);
26+
}
27+
1828
if (expr.type !== 'ObjectExpression') {
1929
return errorLeft(`unimplemented route expression type ${expr.type}`);
2030
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import * as E from 'fp-ts/lib/Either';
2+
import assert from 'node:assert/strict';
3+
import test from 'node:test';
4+
import type { NestedDirectoryJSON } from 'memfs';
5+
6+
import { TestProject } from './testProject';
7+
import { parseApiSpec, type Route } from '../src';
8+
import { MOCK_NODE_MODULES_DIR } from './externalModules';
9+
import { stripStacktraceOfErrors } from '../src/error';
10+
11+
async function testCase(
12+
description: string,
13+
files: NestedDirectoryJSON,
14+
entryPoint: string,
15+
expected: Record<string, Route[]>,
16+
expectedErrors: string[] = [],
17+
) {
18+
test(description, async () => {
19+
const project = new TestProject({ ...files, ...MOCK_NODE_MODULES_DIR });
20+
21+
await project.parseEntryPoint(entryPoint);
22+
const sourceFile = project.get(entryPoint);
23+
if (sourceFile === undefined) {
24+
throw new Error(`could not find source file ${entryPoint}`);
25+
}
26+
27+
const actual: Record<string, Route[]> = {};
28+
const errors: string[] = [];
29+
for (const symbol of sourceFile.symbols.declarations) {
30+
if (symbol.init !== undefined) {
31+
if (symbol.init.type !== 'CallExpression') {
32+
continue;
33+
} else if (
34+
symbol.init.callee.type !== 'MemberExpression' ||
35+
symbol.init.callee.property.type !== 'Identifier' ||
36+
symbol.init.callee.property.value !== 'apiSpec'
37+
) {
38+
continue;
39+
} else if (symbol.init.arguments.length !== 1) {
40+
continue;
41+
}
42+
const arg = symbol.init.arguments[0]!;
43+
// Note: This test should handle identifiers, but currently fails
44+
const result = parseApiSpec(project, sourceFile, arg.expression);
45+
if (E.isLeft(result)) {
46+
errors.push(result.left);
47+
} else {
48+
actual[symbol.name] = result.right;
49+
}
50+
}
51+
}
52+
53+
assert.deepEqual(stripStacktraceOfErrors(errors), expectedErrors);
54+
assert.deepEqual(actual, expected);
55+
});
56+
}
57+
58+
const IDENTIFIER_API_SPEC = {
59+
'/index.ts': `
60+
import * as t from 'io-ts';
61+
import * as h from '@api-ts/io-ts-http';
62+
63+
const myApiSpecProps = {
64+
'api.test': {
65+
get: h.httpRoute({
66+
path: '/test',
67+
method: 'GET',
68+
request: h.httpRequest({}),
69+
response: {
70+
200: t.string,
71+
},
72+
})
73+
}
74+
};
75+
76+
// This fails due to Identifier type not being handled
77+
export const test = h.apiSpec(myApiSpecProps);`,
78+
};
79+
80+
testCase(
81+
'identifier api spec',
82+
IDENTIFIER_API_SPEC,
83+
'/index.ts',
84+
{
85+
test: [
86+
{
87+
path: '/test',
88+
method: 'GET',
89+
parameters: [],
90+
response: { 200: { type: 'string', primitive: true } },
91+
},
92+
],
93+
},
94+
[],
95+
);
96+
97+
const WORKAROUND_API_SPEC = {
98+
'/index.ts': `
99+
import * as t from 'io-ts';
100+
import * as h from '@api-ts/io-ts-http';
101+
102+
const myApiSpecProps = {
103+
'api.test': {
104+
get: h.httpRoute({
105+
path: '/test',
106+
method: 'GET',
107+
request: h.httpRequest({}),
108+
response: {
109+
200: t.string,
110+
},
111+
})
112+
}
113+
};
114+
115+
// This works with the workaround
116+
export const test = h.apiSpec({
117+
...myApiSpecProps
118+
});`,
119+
};
120+
121+
testCase('workaround api spec', WORKAROUND_API_SPEC, '/index.ts', {
122+
test: [
123+
{
124+
path: '/test',
125+
method: 'GET',
126+
parameters: [],
127+
response: { 200: { type: 'string', primitive: true } },
128+
},
129+
],
130+
});
131+
132+
const NESTED_IDENTIFIER_API_SPEC = {
133+
'/index.ts': `
134+
import * as t from 'io-ts';
135+
import * as h from '@api-ts/io-ts-http';
136+
137+
const routeSpec = h.httpRoute({
138+
path: '/test',
139+
method: 'GET',
140+
request: h.httpRequest({}),
141+
response: {
142+
200: t.string,
143+
},
144+
});
145+
146+
const routeObj = {
147+
get: routeSpec
148+
};
149+
150+
const myApiSpecProps = {
151+
'api.test': routeObj
152+
};
153+
154+
// This should now work with our fix
155+
export const test = h.apiSpec(myApiSpecProps);`,
156+
};
157+
158+
testCase('nested identifier api spec', NESTED_IDENTIFIER_API_SPEC, '/index.ts', {
159+
test: [
160+
{
161+
path: '/test',
162+
method: 'GET',
163+
parameters: [],
164+
response: { 200: { type: 'string', primitive: true } },
165+
},
166+
],
167+
});
168+
169+
const IMPORTED_IDENTIFIER_API_SPEC = {
170+
'/routes.ts': `
171+
import * as t from 'io-ts';
172+
import * as h from '@api-ts/io-ts-http';
173+
174+
export const apiRoutes = {
175+
'api.test': {
176+
get: h.httpRoute({
177+
path: '/test',
178+
method: 'GET',
179+
request: h.httpRequest({}),
180+
response: {
181+
200: t.string,
182+
},
183+
})
184+
}
185+
};
186+
`,
187+
'/index.ts': `
188+
import * as h from '@api-ts/io-ts-http';
189+
import { apiRoutes } from './routes';
190+
191+
// This should now work with our fix
192+
export const test = h.apiSpec(apiRoutes);`,
193+
};
194+
195+
testCase('imported identifier api spec', IMPORTED_IDENTIFIER_API_SPEC, '/index.ts', {
196+
test: [
197+
{
198+
path: '/test',
199+
method: 'GET',
200+
parameters: [],
201+
response: { 200: { type: 'string', primitive: true } },
202+
},
203+
],
204+
});

0 commit comments

Comments
 (0)