Skip to content

Commit 3410740

Browse files
committed
test: test fallback to custom codecs when arrow function inlining fails
Check if parseFunctionBody() succeeded before returning, allowing parser to use custom codec definitions when function contains parameter references.
1 parent 97bb096 commit 3410740

File tree

2 files changed

+286
-0
lines changed

2 files changed

+286
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const E = require('fp-ts/lib/Either');
2+
const assert = require('node:assert/strict');
3+
const test = require('node:test');
4+
const { TestProject } = require('./testProject');
5+
const { parsePlainInitializer } = require('../src');
6+
7+
test('arrow function with parameters should use custom codec definition', async () => {
8+
const files = {
9+
'/api/v2/admin/migrateOrgShardKey.ts': `
10+
import * as t from 'io-ts';
11+
import { NonEmptyString } from './types';
12+
import { arrayFromArrayOrSingle } from '../../../utils';
13+
14+
export const UpdateShardKeyRequestBody = {
15+
shardKey: t.string,
16+
collectionType: t.union([
17+
t.literal('user'),
18+
t.literal('enterprise'),
19+
t.literal('enterpriseTemplate')
20+
]),
21+
ids: arrayFromArrayOrSingle(NonEmptyString),
22+
enterpriseUpdateUsers: t.boolean,
23+
} as const;
24+
`,
25+
'/api/v2/admin/types.ts': `
26+
import * as t from 'io-ts';
27+
28+
export const NonEmptyString = t.string;
29+
`,
30+
'/utils/arrayFromArrayOrSingle.ts': `
31+
import * as t from 'io-ts';
32+
import { arrayFromSingle } from '@bitgo/public-types';
33+
34+
export const arrayFromArrayOrSingle = <C extends t.Mixed>(codec: C) =>
35+
t.union([t.array(codec), arrayFromSingle(codec)]);
36+
`,
37+
};
38+
39+
const knownImports = {
40+
'.': {
41+
arrayFromArrayOrSingle: (_: any, innerSchema: any) =>
42+
E.right({ type: 'array', items: innerSchema }),
43+
},
44+
};
45+
46+
const project = new TestProject(files, knownImports);
47+
await project.parseEntryPoint('/api/v2/admin/migrateOrgShardKey.ts');
48+
const sourceFile = project.get('/api/v2/admin/migrateOrgShardKey.ts');
49+
50+
assert.ok(sourceFile !== undefined, 'Source file should exist');
51+
52+
const actual: any = {};
53+
const errors: string[] = [];
54+
55+
for (const symbol of sourceFile.symbols.declarations) {
56+
if (symbol.init !== undefined) {
57+
const result = parsePlainInitializer(project, sourceFile, symbol.init);
58+
if (E.isLeft(result)) {
59+
errors.push(result.left);
60+
} else {
61+
actual[symbol.name] = result.right;
62+
}
63+
}
64+
}
65+
66+
assert.deepEqual(
67+
errors,
68+
[],
69+
`Expected no errors, but got "${errors[0]}"`
70+
);
71+
72+
assert.ok(
73+
actual.UpdateShardKeyRequestBody?.type === 'object',
74+
'Should parse UpdateShardKeyRequestBody as object'
75+
);
76+
77+
assert.ok(
78+
actual.UpdateShardKeyRequestBody?.properties?.ids?.type === 'array',
79+
'Should parse ids field as array (from custom codec definition)'
80+
);
81+
});
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { testCase } from './testHarness';
2+
3+
// SUCCESS CASE: Parameter-less arrow function should be parsed
4+
testCase(
5+
'parameter-less arrow function is parsed and inlined',
6+
`
7+
import * as t from 'io-ts';
8+
9+
const StringFactory = () => t.string;
10+
11+
export const MyCodec = t.type({
12+
field: StringFactory(),
13+
});
14+
`,
15+
{
16+
openapi: '3.0.3',
17+
info: {
18+
title: 'Test',
19+
version: '1.0.0',
20+
},
21+
paths: {},
22+
components: {
23+
schemas: {
24+
MyCodec: {
25+
title: 'MyCodec',
26+
type: 'object',
27+
properties: {
28+
field: {
29+
type: 'string',
30+
},
31+
},
32+
required: ['field'],
33+
},
34+
StringFactory: {
35+
title: 'StringFactory',
36+
},
37+
},
38+
},
39+
},
40+
);
41+
42+
// SUCCESS CASE: Parameter-less with BlockStatement
43+
testCase(
44+
'parameter-less arrow function with BlockStatement is parsed',
45+
`
46+
import * as t from 'io-ts';
47+
48+
const ComplexFactory = () => {
49+
return t.union([t.literal('true'), t.literal('false')]);
50+
};
51+
52+
export const MyCodec = t.type({
53+
field: ComplexFactory(),
54+
});
55+
`,
56+
{
57+
openapi: '3.0.3',
58+
info: {
59+
title: 'Test',
60+
version: '1.0.0',
61+
},
62+
paths: {},
63+
components: {
64+
schemas: {
65+
MyCodec: {
66+
title: 'MyCodec',
67+
type: 'object',
68+
properties: {
69+
field: {
70+
type: 'string',
71+
enum: ['true', 'false'],
72+
},
73+
},
74+
required: ['field'],
75+
},
76+
ComplexFactory: {
77+
title: 'ComplexFactory',
78+
},
79+
},
80+
},
81+
},
82+
);
83+
84+
// FALLBACK CASE: Arrow function with parameters should fallback to ref (not error)
85+
testCase(
86+
'arrow function with parameters falls back to ref without error',
87+
`
88+
import * as t from 'io-ts';
89+
90+
const optional = (codec) => t.union([codec, t.undefined]);
91+
92+
export const MyCodec = t.type({
93+
field: optional(t.string),
94+
});
95+
`,
96+
{
97+
openapi: '3.0.3',
98+
info: {
99+
title: 'Test',
100+
version: '1.0.0',
101+
},
102+
paths: {},
103+
components: {
104+
schemas: {
105+
MyCodec: {
106+
title: 'MyCodec',
107+
type: 'object',
108+
properties: {
109+
field: {
110+
$ref: '#/components/schemas/optional',
111+
},
112+
},
113+
required: ['field'],
114+
},
115+
optional: {
116+
title: 'optional',
117+
},
118+
},
119+
},
120+
},
121+
);
122+
123+
// FALLBACK CASE: Parameter-less calling function with parameters
124+
testCase(
125+
'parameter-less function calling function with parameters falls back gracefully',
126+
`
127+
import * as t from 'io-ts';
128+
129+
const optional = (codec) => t.union([codec, t.undefined]);
130+
const makeOptionalString = () => optional(t.string);
131+
132+
export const MyCodec = t.type({
133+
field: makeOptionalString(),
134+
});
135+
`,
136+
{
137+
openapi: '3.0.3',
138+
info: {
139+
title: 'Test',
140+
version: '1.0.0',
141+
},
142+
paths: {},
143+
components: {
144+
schemas: {
145+
MyCodec: {
146+
title: 'MyCodec',
147+
type: 'object',
148+
properties: {
149+
field: {
150+
$ref: '#/components/schemas/optional',
151+
},
152+
},
153+
required: ['field'],
154+
},
155+
makeOptionalString: {
156+
title: 'makeOptionalString',
157+
},
158+
optional: {
159+
title: 'optional',
160+
},
161+
},
162+
},
163+
},
164+
);
165+
166+
// FALLBACK CASE: BlockStatement without return should fallback (not error)
167+
testCase(
168+
'BlockStatement without return falls back to ref without error',
169+
`
170+
import * as t from 'io-ts';
171+
172+
const NoReturnFactory = () => {
173+
const x = t.string;
174+
};
175+
176+
export const MyCodec = t.type({
177+
field: NoReturnFactory(),
178+
});
179+
`,
180+
{
181+
openapi: '3.0.3',
182+
info: {
183+
title: 'Test',
184+
version: '1.0.0',
185+
},
186+
paths: {},
187+
components: {
188+
schemas: {
189+
MyCodec: {
190+
title: 'MyCodec',
191+
type: 'object',
192+
properties: {
193+
field: {
194+
$ref: '#/components/schemas/NoReturnFactory',
195+
},
196+
},
197+
required: ['field'],
198+
},
199+
NoReturnFactory: {
200+
title: 'NoReturnFactory',
201+
},
202+
},
203+
},
204+
},
205+
);

0 commit comments

Comments
 (0)