Skip to content

Commit 86c8d8d

Browse files
feat(openapi-generator): support Identifier type in route expressions
2 parents 7e9d215 + c6fdcbe commit 86c8d8d

File tree

2 files changed

+210
-0
lines changed

2 files changed

+210
-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: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
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+
export const test = h.apiSpec(myApiSpecProps);`,
77+
};
78+
79+
testCase(
80+
'identifier api spec',
81+
IDENTIFIER_API_SPEC,
82+
'/index.ts',
83+
{
84+
test: [
85+
{
86+
path: '/test',
87+
method: 'GET',
88+
parameters: [],
89+
response: { 200: { type: 'string', primitive: true } },
90+
},
91+
],
92+
},
93+
[],
94+
);
95+
96+
const WORKAROUND_API_SPEC = {
97+
'/index.ts': `
98+
import * as t from 'io-ts';
99+
import * as h from '@api-ts/io-ts-http';
100+
101+
const myApiSpecProps = {
102+
'api.test': {
103+
get: h.httpRoute({
104+
path: '/test',
105+
method: 'GET',
106+
request: h.httpRequest({}),
107+
response: {
108+
200: t.string,
109+
},
110+
})
111+
}
112+
};
113+
114+
export const test = h.apiSpec({
115+
...myApiSpecProps
116+
});`,
117+
};
118+
119+
testCase('workaround api spec', WORKAROUND_API_SPEC, '/index.ts', {
120+
test: [
121+
{
122+
path: '/test',
123+
method: 'GET',
124+
parameters: [],
125+
response: { 200: { type: 'string', primitive: true } },
126+
},
127+
],
128+
});
129+
130+
const NESTED_IDENTIFIER_API_SPEC = {
131+
'/index.ts': `
132+
import * as t from 'io-ts';
133+
import * as h from '@api-ts/io-ts-http';
134+
135+
const routeSpec = h.httpRoute({
136+
path: '/test',
137+
method: 'GET',
138+
request: h.httpRequest({}),
139+
response: {
140+
200: t.string,
141+
},
142+
});
143+
144+
const routeObj = {
145+
get: routeSpec
146+
};
147+
148+
const myApiSpecProps = {
149+
'api.test': routeObj
150+
};
151+
152+
export const test = h.apiSpec(myApiSpecProps);`,
153+
};
154+
155+
testCase('nested identifier api spec', NESTED_IDENTIFIER_API_SPEC, '/index.ts', {
156+
test: [
157+
{
158+
path: '/test',
159+
method: 'GET',
160+
parameters: [],
161+
response: { 200: { type: 'string', primitive: true } },
162+
},
163+
],
164+
});
165+
166+
const IMPORTED_IDENTIFIER_API_SPEC = {
167+
'/routes.ts': `
168+
import * as t from 'io-ts';
169+
import * as h from '@api-ts/io-ts-http';
170+
171+
export const apiRoutes = {
172+
'api.test': {
173+
get: h.httpRoute({
174+
path: '/test',
175+
method: 'GET',
176+
request: h.httpRequest({}),
177+
response: {
178+
200: t.string,
179+
},
180+
})
181+
}
182+
};
183+
`,
184+
'/index.ts': `
185+
import * as h from '@api-ts/io-ts-http';
186+
import { apiRoutes } from './routes';
187+
188+
export const test = h.apiSpec(apiRoutes);`,
189+
};
190+
191+
testCase('imported identifier api spec', IMPORTED_IDENTIFIER_API_SPEC, '/index.ts', {
192+
test: [
193+
{
194+
path: '/test',
195+
method: 'GET',
196+
parameters: [],
197+
response: { 200: { type: 'string', primitive: true } },
198+
},
199+
],
200+
});

0 commit comments

Comments
 (0)