Skip to content

Commit a252f30

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 a252f30

2 files changed

Lines changed: 215 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)