Skip to content

Commit 7144707

Browse files
authored
feat(mock): emit strict Mock return types when required and nonNullable are both true (#3529)
1 parent 6f82953 commit 7144707

43 files changed

Lines changed: 2196 additions & 26 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/core/src/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,7 @@ export interface GeneratorMockOutput {
13051305
type: OutputMockType;
13061306
implementation: string;
13071307
imports: GeneratorImport[];
1308+
strictMockSchemaTypeNames?: string[];
13081309
}
13091310

13101311
export interface GeneratorMockOutputFull {
@@ -1315,6 +1316,7 @@ export interface GeneratorMockOutputFull {
13151316
handlerName: string;
13161317
};
13171318
imports: GeneratorImport[];
1319+
strictMockSchemaTypeNames?: string[];
13181320
}
13191321

13201322
export interface GeneratorTarget {
@@ -1482,6 +1484,7 @@ export interface ClientMockGeneratorImplementation {
14821484
export interface ClientMockGeneratorBuilder {
14831485
imports: GeneratorImport[];
14841486
implementation: ClientMockGeneratorImplementation;
1487+
strictMockSchemaTypeNames?: string[];
14851488
}
14861489

14871490
export type ClientMockBuilder = (
@@ -1644,6 +1647,11 @@ export type ResReqTypesValue = ScalarValue & {
16441647
originalSchema?: OpenApiSchemaObject;
16451648
};
16461649

1650+
export interface FinalizeMockImplementationOptions {
1651+
mockOptions?: Pick<MockOptions, 'required' | 'nonNullable'>;
1652+
strictSchemaTypeNames?: readonly string[];
1653+
}
1654+
16471655
export interface WriteSpecBuilder {
16481656
operations: GeneratorOperations;
16491657
verbOptions: Record<string, GeneratorVerbOptions>;
@@ -1653,6 +1661,11 @@ export interface WriteSpecBuilder {
16531661
footer: GeneratorClientFooter;
16541662
imports: GeneratorClientImports;
16551663
importsMock: GenerateMockImports;
1664+
/** Hoists shared strict-mock type aliases once per aggregated mock file. */
1665+
finalizeMockImplementation?: (
1666+
implementation: string,
1667+
options: FinalizeMockImplementationOptions,
1668+
) => string;
16561669
extraFiles: ClientFileBuilder[];
16571670
info: OpenApiInfoObject;
16581671
target: string;
@@ -1740,6 +1753,11 @@ export type GeneratorApiBuilder = GeneratorApiOperations & {
17401753
footer: GeneratorClientFooter;
17411754
imports: GeneratorClientImports;
17421755
importsMock: GenerateMockImports;
1756+
/** Hoists shared strict-mock type aliases once per aggregated mock file. */
1757+
finalizeMockImplementation?: (
1758+
implementation: string,
1759+
options: FinalizeMockImplementationOptions,
1760+
) => string;
17431761
extraFiles: ClientFileBuilder[];
17441762
};
17451763

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type {
2+
FinalizeMockImplementationOptions,
3+
GeneratorMockOutput,
4+
NormalizedOutputOptions,
5+
} from '../types';
6+
7+
type MockOutputWithStrictNames = Pick<
8+
GeneratorMockOutput,
9+
'strictMockSchemaTypeNames'
10+
>;
11+
12+
export function getFinalizeMockImplementationOptions(
13+
output: NormalizedOutputOptions,
14+
mockOutputs: MockOutputWithStrictNames | readonly MockOutputWithStrictNames[],
15+
): FinalizeMockImplementationOptions {
16+
const outputs: readonly MockOutputWithStrictNames[] = Array.isArray(
17+
mockOutputs,
18+
)
19+
? mockOutputs
20+
: [mockOutputs];
21+
const strictSchemaTypeNames = [
22+
...new Set(
23+
outputs.flatMap(
24+
(mockOutput) => mockOutput.strictMockSchemaTypeNames ?? [],
25+
),
26+
),
27+
];
28+
29+
return {
30+
mockOptions: output.override.mock,
31+
strictSchemaTypeNames:
32+
strictSchemaTypeNames.length > 0 ? strictSchemaTypeNames : undefined,
33+
};
34+
}

packages/core/src/writers/single-mode.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../utils';
1111
import { escapeRegExp } from '../utils/string';
1212
import { writeGeneratedFile } from './file';
13+
import { getFinalizeMockImplementationOptions } from './finalize-mock-implementation';
1314
import { generateImportsForBuilder } from './generate-imports-for-builder';
1415
import { collapseInlineMockOutputs } from './mock-outputs';
1516
import { generateTarget } from './target';
@@ -56,6 +57,12 @@ export async function writeSingleMode({
5657
const implementationMock = mockOutputs
5758
.map((m) => m.implementation)
5859
.join('\n\n');
60+
const finalizedImplementationMock = builder.finalizeMockImplementation
61+
? builder.finalizeMockImplementation(
62+
implementationMock,
63+
getFinalizeMockImplementationOptions(output, mockOutputs),
64+
)
65+
: implementationMock;
5966
// Aggregate imports across all mock entries for the value-import promotion
6067
// pass below.
6168
const importsMock = mockOutputs.flatMap((m) => m.imports);
@@ -222,7 +229,7 @@ export async function writeSingleMode({
222229

223230
if (mockOutputs.length > 0) {
224231
data += '\n\n';
225-
data += implementationMock;
232+
data += finalizedImplementationMock;
226233
}
227234

228235
await writeGeneratedFile(path, data);

packages/core/src/writers/split-mode.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from '../utils';
1717
import { getMockFileExtensionByTypeName } from '../utils/file-extensions';
1818
import { writeGeneratedFile } from './file';
19+
import { getFinalizeMockImplementationOptions } from './finalize-mock-implementation';
1920
import { generateImportsForBuilder } from './generate-imports-for-builder';
2021
import { generateTarget } from './target';
2122
import { getOrvalGeneratedTypes, getTypedResponse } from './types';
@@ -184,15 +185,21 @@ export async function writeSplitMode({
184185
relativeSchemasPath,
185186
);
186187
let mockData = header;
188+
const finalizedMockImplementation = builder.finalizeMockImplementation
189+
? builder.finalizeMockImplementation(
190+
mockOutput.implementation,
191+
getFinalizeMockImplementationOptions(output, mockOutput),
192+
)
193+
: mockOutput.implementation;
187194
mockData += builder.importsMock({
188-
implementation: mockOutput.implementation,
195+
implementation: finalizedMockImplementation,
189196
imports: importsMockForBuilder,
190197
projectName,
191198
hasSchemaDir: !!output.schemas,
192199
isAllowSyntheticDefaultImports,
193200
options: entry,
194201
});
195-
mockData += `\n${mockOutput.implementation}`;
202+
mockData += `\n${finalizedMockImplementation}`;
196203

197204
const mockPath = path.join(
198205
dirname,

packages/core/src/writers/split-tags-mode.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from '../utils';
2121
import { getMockFileExtensionByTypeName } from '../utils/file-extensions';
2222
import { writeGeneratedFile } from './file';
23+
import { getFinalizeMockImplementationOptions } from './finalize-mock-implementation';
2324
import { generateImportsForBuilder } from './generate-imports-for-builder';
2425
import { generateTargetForTags } from './target-tags';
2526
import { getOrvalGeneratedTypes, getTypedResponse } from './types';
@@ -264,16 +265,22 @@ export async function writeSplitTagsMode({
264265
relativeSchemasPath,
265266
);
266267

268+
const finalizedMockImplementation = builder.finalizeMockImplementation
269+
? builder.finalizeMockImplementation(
270+
mockOutput.implementation,
271+
getFinalizeMockImplementationOptions(output, mockOutput),
272+
)
273+
: mockOutput.implementation;
267274
let mockData = header;
268275
mockData += builder.importsMock({
269-
implementation: mockOutput.implementation,
276+
implementation: finalizedMockImplementation,
270277
imports: importsMockForBuilder,
271278
projectName,
272279
hasSchemaDir: !!output.schemas,
273280
isAllowSyntheticDefaultImports,
274281
options: entry,
275282
});
276-
mockData += `\n${mockOutput.implementation}`;
283+
mockData += `\n${finalizedMockImplementation}`;
277284

278285
const mockPath = path.join(
279286
dirname,

packages/core/src/writers/tags-mode.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from '../utils';
1414
import { escapeRegExp } from '../utils/string';
1515
import { writeGeneratedFile } from './file';
16+
import { getFinalizeMockImplementationOptions } from './finalize-mock-implementation';
1617
import { generateImportsForBuilder } from './generate-imports-for-builder';
1718
import { collapseInlineMockOutputs } from './mock-outputs';
1819
import { generateTargetForTags } from './target-tags';
@@ -70,6 +71,12 @@ export async function writeTagsMode({
7071
const implementationMock = mockOutputs
7172
.map((m) => m.implementation)
7273
.join('\n\n');
74+
const finalizedImplementationMock = builder.finalizeMockImplementation
75+
? builder.finalizeMockImplementation(
76+
implementationMock,
77+
getFinalizeMockImplementationOptions(output, mockOutputs),
78+
)
79+
: implementationMock;
7380

7481
let data = header;
7582

@@ -231,7 +238,7 @@ export async function writeTagsMode({
231238
if (mockOutputs.length > 0) {
232239
data += '\n\n';
233240

234-
data += implementationMock;
241+
data += finalizedImplementationMock;
235242
}
236243

237244
const implementationPath = path.join(

packages/core/src/writers/target-tags.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function flattenMockOutput(full: GeneratorMockOutputFull): GeneratorMockOutput {
3939
type: full.type,
4040
implementation: full.implementation.function + full.implementation.handler,
4141
imports: full.imports,
42+
strictMockSchemaTypeNames: full.strictMockSchemaTypeNames,
4243
};
4344
}
4445

@@ -58,6 +59,14 @@ function mergeOperationMockOutputs(
5859
result.push(acc);
5960
}
6061
acc.imports.push(...op.imports);
62+
if (op.strictMockSchemaTypeNames?.length) {
63+
acc.strictMockSchemaTypeNames = [
64+
...new Set([
65+
...(acc.strictMockSchemaTypeNames ?? []),
66+
...op.strictMockSchemaTypeNames,
67+
]),
68+
];
69+
}
6170
acc.implementation.function += op.implementation.function;
6271
acc.implementation.handler += op.implementation.handler;
6372
if (op.implementation.handlerName) {

packages/core/src/writers/target.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function flattenMockOutput(full: GeneratorMockOutputFull): GeneratorMockOutput {
2424
type: full.type,
2525
implementation: full.implementation.function + full.implementation.handler,
2626
imports: full.imports,
27+
strictMockSchemaTypeNames: full.strictMockSchemaTypeNames,
2728
};
2829
}
2930

@@ -70,6 +71,14 @@ export function generateTarget(
7071
target.mockOutputs.push(acc);
7172
}
7273
acc.imports.push(...opMock.imports);
74+
if (opMock.strictMockSchemaTypeNames?.length) {
75+
acc.strictMockSchemaTypeNames = [
76+
...new Set([
77+
...(acc.strictMockSchemaTypeNames ?? []),
78+
...opMock.strictMockSchemaTypeNames,
79+
]),
80+
];
81+
}
7382
acc.implementation.function += opMock.implementation.function;
7483
acc.implementation.handler += opMock.implementation.handler;
7584
if (opMock.implementation.handlerName) {

packages/mock/src/faker/getters/array-item-factory.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import {
1313
resolveRef,
1414
} from '@orval/core';
1515

16+
import {
17+
formatMockFactoryDeclaration,
18+
getMockFactorySignatureParts,
19+
} from '../../mock-types';
1620
import type { MockSchema } from '../../types';
1721
import { overrideVarName } from './object';
1822
import { extractItemsRef } from './scalar';
@@ -300,11 +304,24 @@ export function extractArrayItemMock({
300304
);
301305

302306
if (!alreadyExtracted) {
303-
const args = `${overrideVarName}: Partial<${typeName}> = {}`;
307+
const mockOptions = context.output.override.mock;
308+
const { param, returnType, returnCast } = getMockFactorySignatureParts(
309+
typeName,
310+
mockOptions,
311+
{
312+
isOverridable: true,
313+
overrideType: `Partial<${typeName}>`,
314+
},
315+
);
304316
const spreadPrefix = mapValue.startsWith('...') ? '' : '...';
305-
const func =
306-
`export const ${factoryName} = (${args}): ${typeName} => ` +
307-
`({${spreadPrefix}${mapValue}, ...${overrideVarName}});`;
317+
const func = formatMockFactoryDeclaration(
318+
factoryName,
319+
param,
320+
returnType,
321+
`{${spreadPrefix}${mapValue}, ...${overrideVarName}}`,
322+
returnCast,
323+
{ terminateStatement: true },
324+
);
308325
splitMockImplementations.push(func);
309326
fileLevelFactories.add(factoryName);
310327
}

packages/mock/src/faker/index.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import type {
1010
import { isFakerMock, isMswMock, OutputMockType } from '@orval/core';
1111
import { describe, expect, expectTypeOf, it } from 'vitest';
1212

13-
import { generateFaker, generateFakerImports } from './index';
13+
import { createTestContextSpec } from '../../../core/src/test-utils/context';
14+
import {
15+
generateFaker,
16+
generateFakerForSchemas,
17+
generateFakerImports,
18+
} from './index';
1419

1520
const mockVerbOptions = {
1621
operationId: 'getUser',
@@ -155,3 +160,47 @@ describe('discriminated GlobalMockOptions union', () => {
155160
expect(isFakerMock(mock)).toBe(false);
156161
});
157162
});
163+
164+
describe('generateFakerForSchemas strict mock types (#3525)', () => {
165+
const context = createTestContextSpec({
166+
override: {
167+
mock: {
168+
required: true,
169+
nonNullable: true,
170+
},
171+
},
172+
});
173+
174+
it('emits PetMock alias and return type for schema factories', () => {
175+
const result = generateFakerForSchemas(
176+
[
177+
{
178+
name: 'Pet',
179+
model: 'Pet',
180+
imports: [],
181+
schema: {
182+
type: 'object',
183+
required: ['id', 'name'],
184+
properties: {
185+
id: { type: 'integer' },
186+
name: { type: 'string' },
187+
tag: { type: 'string', nullable: true },
188+
},
189+
},
190+
},
191+
],
192+
context,
193+
{ type: OutputMockType.FAKER, schemas: true },
194+
);
195+
196+
expect(result.implementation).toContain('export type PetMock = {');
197+
expect(result.implementation).toContain('export type KeysWithNull<O>');
198+
expect(result.implementation).toContain(
199+
'export const getPetMock = <O extends Partial<Pet> = {}>(overrideResponse?: O): MockWithNullableOverrides<Pet, O, PetMock> =>',
200+
);
201+
expect(result.implementation).toContain(
202+
') as MockWithNullableOverrides<Pet, O, PetMock>;',
203+
);
204+
expect(result.implementation).not.toContain(', null]');
205+
});
206+
});

0 commit comments

Comments
 (0)