Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions packages/mock/src/faker/resolvers/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,16 @@ export function resolveMockValue({
// so the spread form keeps callers (combineSchemasMock, object
// properties) working without other changes. For everything else
// (scalars, arrays, nullables) emit the bare call.
//
// A `oneOf`/`anyOf` is only object-like when *every* branch resolves to
// an object. A composition of primitives (e.g. `number | string`) makes
// the factory return a primitive union, which is not spreadable: emitting
// `{ ...get<X>Mock() }` is invalid TypeScript (TS2698) and would discard
// the value as `{}` at runtime, so it must use the bare call (#3200).
const isObjectLike =
newSchema.type === 'object' ||
!!newSchema.allOf ||
!!newSchema.oneOf ||
!!newSchema.anyOf;
resolvesToObjectLike(newSchema, context);
const callValue = isObjectLike
? `{ ...${factoryName}() }`
: `${factoryName}()`;
Expand Down Expand Up @@ -449,3 +454,62 @@ function getType(schema: MockSchema) {
(schema.properties ? 'object' : schema.items ? 'array' : undefined)
);
}

// Whether a schema (or a `$ref` to one) ultimately produces an object mock.
// Used to decide if a delegated `get<X>Mock()` call may be spread into an
// object literal. Object-like schemas are `type: 'object'`, `properties`,
// `additionalProperties` and `allOf`; a `oneOf`/`anyOf` qualifies only when
// every branch resolves to an object. A union containing a primitive
// (e.g. `number | string`) does not, since spreading that union is invalid
// TypeScript. `seen` carries the `$ref`s on the current resolution path to
// guard against self-referential compositions. A fresh copy is taken at each
// `$ref` hop so sibling branches sharing a `$ref` don't falsely trip the guard.
function resolvesToObjectLike(
schema: MockSchema,
context: ContextSpec,
seen = new Set<string>(),
): boolean {
let resolved: Partial<OpenApiSchemaObject> | undefined;

if (isReference(schema)) {
// A non-string or already-visited `$ref` can't be resolved further here.
if (typeof schema.$ref !== 'string' || seen.has(schema.$ref)) {
return false;
}
seen = new Set(seen).add(schema.$ref);
const { refPaths } = getRefInfo(schema.$ref, context);
resolved = Array.isArray(refPaths)
? (prop(
context.spec,
// @ts-expect-error: refPaths are not guaranteed to be valid keys of the spec
...refPaths,
) as Partial<OpenApiSchemaObject>)
: undefined;
} else {
resolved = schema as Partial<OpenApiSchemaObject>;
}

if (!resolved) {
return false;
}

if (
resolved.type === 'object' ||
resolved.properties ||
resolved.additionalProperties ||
resolved.allOf
) {
return true;
}

const branches = (resolved.oneOf ?? resolved.anyOf) as
| MockSchema[]
| undefined;
if (branches && branches.length > 0) {
return branches.every((branch) =>
resolvesToObjectLike(branch, context, seen),
);
}

return false;
}
48 changes: 48 additions & 0 deletions tests/__snapshots__/mock/issue-3200/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

import type { StringToIntegerMap, StringToNumberMap } from './model';

import { faker } from '@faker-js/faker';

import { getIntegerLikeMock, getNumberLikeMock } from './model/index.faker';

export const getIssue3200 = (axiosInstance: AxiosInstance = axios) => {
const getIntegerMap = (
options?: AxiosRequestConfig,
): Promise<AxiosResponse<StringToIntegerMap>> => {
return axiosInstance.get(`/integer-map`, options);
};

const getNumberMap = (
options?: AxiosRequestConfig,
): Promise<AxiosResponse<StringToNumberMap>> => {
return axiosInstance.get(`/number-map`, options);
};

return { getIntegerMap, getNumberMap };
};
export type GetIntegerMapResult = AxiosResponse<StringToIntegerMap>;
export type GetNumberMapResult = AxiosResponse<StringToNumberMap>;

export const getGetIntegerMapResponseMock = (): StringToIntegerMap => ({
[faker.string.alphanumeric(5)]: getIntegerLikeMock(),
});

export const getGetNumberMapResponseMock = (): StringToNumberMap => ({
[faker.string.alphanumeric(5)]: getNumberLikeMock(),
});
42 changes: 42 additions & 0 deletions tests/__snapshots__/mock/issue-3200/model/index.faker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/
import { faker } from '@faker-js/faker';

import type {
IntegerLike,
NumberLike,
StringToIntegerMap,
StringToNumberMap,
} from '.';

export const getIntegerLikeMock = (): IntegerLike =>
faker.helpers.arrayElement([
faker.number.int(),
faker.string.alpha({ length: { min: 10, max: 20 } }),
]);

export const getStringToIntegerMapMock = (): StringToIntegerMap => ({
[faker.string.alphanumeric(5)]: getIntegerLikeMock(),
});

export const getNumberLikeMock = (): NumberLike =>
faker.helpers.arrayElement([
faker.number.float({ fractionDigits: 2 }),
faker.string.alpha({ length: { min: 10, max: 20 } }),
]);

export const getStringToNumberMapMock = (): StringToNumberMap => ({
[faker.string.alphanumeric(5)]: getNumberLikeMock(),
});
19 changes: 19 additions & 0 deletions tests/__snapshots__/mock/issue-3200/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/

export * from './integerLike';
export * from './numberLike';
export * from './stringToIntegerMap';
export * from './stringToNumberMap';
16 changes: 16 additions & 0 deletions tests/__snapshots__/mock/issue-3200/model/integerLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/

export type IntegerLike = number | string;
16 changes: 16 additions & 0 deletions tests/__snapshots__/mock/issue-3200/model/numberLike.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/

export type NumberLike = number | string;
19 changes: 19 additions & 0 deletions tests/__snapshots__/mock/issue-3200/model/stringToIntegerMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/
import type { IntegerLike } from './integerLike';

export interface StringToIntegerMap {
[key: string]: IntegerLike;
}
19 changes: 19 additions & 0 deletions tests/__snapshots__/mock/issue-3200/model/stringToNumberMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Generated by orval v8.14.0 🍺
* Do not edit manually.
* Issue 3200
* Reproduces the case where an object schema used as a dictionary
* (`additionalProperties: { $ref: <primitive union> }`) generates an invalid
* faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
* `{ ...get<X>Mock() }`, but the factory returns a primitive union
* (e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
* yields `{}` at runtime. The generated dictionary value should be the bare
* `get<X>Mock()` call instead.
*
* OpenAPI spec version: 1.0.0
*/
import type { NumberLike } from './numberLike';

export interface StringToNumberMap {
[key: string]: NumberLike;
}
27 changes: 27 additions & 0 deletions tests/api-generation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,33 @@ test('mock issue-3484 required nullable scalars get a single null branch', async
);
});

test('mock issue-3200 dictionary values delegate to a bare factory call for primitive-union $refs', async () => {
// Regression for #3200: with `schemas: true`, an `additionalProperties`
// dictionary whose value is a `$ref` to a primitive `oneOf`/`anyOf`
// (e.g. `number | string`) delegated to `get<X>Mock()`. The delegation
// wrapped the call in `{ ...get<X>Mock() }`, but the factory returns a
// primitive union which is not spreadable: that is invalid TypeScript
// (TS2698, enforced by scripts/typecheck-generated.mjs) and would discard
// the value as `{}` at runtime. The dictionary value must be the bare call.
const content = await readFile(
generated('mock', 'issue-3200', 'model', 'index.faker.ts'),
'utf8',
);

// Whitespace-tolerant so the assertion survives formatter/generator tweaks
// while still pinning the behavior: the dictionary value is the bare call.
expect(content).toMatch(
/\[faker\.string\.alphanumeric\(5\)\]:\s*getIntegerLikeMock\(\)/,
);
expect(content).toMatch(
/\[faker\.string\.alphanumeric\(5\)\]:\s*getNumberLikeMock\(\)/,
);
// The primitive-union factory call must never be spread into the object,
// regardless of how the braces would be formatted.
expect(content).not.toContain('...getIntegerLikeMock()');
expect(content).not.toContain('...getNumberLikeMock()');
});

test('zod issue-3171 applies required from a sibling allOf member to $ref base props', async () => {
// `User`/`UserFull` define their properties in a $ref base (UserBase) and
// carry `required` only in a sibling allOf member. The required array must be
Expand Down
17 changes: 17 additions & 0 deletions tests/configs/mock.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,23 @@ export default defineConfig({
target: '../specifications/faker-schemas-string-enum-ref.yaml',
},
},
issue3200: {
output: {
target: '../generated/mock/issue-3200/endpoints.ts',
schemas: '../generated/mock/issue-3200/model',
client: 'axios',
mock: {
generators: [
{ type: 'faker', schemas: true, operationResponses: true },
],
},
clean: true,
formatter: 'prettier',
},
input: {
target: '../specifications/issue-3200.yaml',
},
},
issue2465: {
output: {
target: '../generated/mock/issue-2465/endpoints.ts',
Expand Down
53 changes: 53 additions & 0 deletions tests/specifications/issue-3200.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
openapi: 3.0.0
info:
title: Issue 3200
description: |
Reproduces the case where an object schema used as a dictionary
(`additionalProperties: { $ref: <primitive union> }`) generates an invalid
faker schema mock under `schemas: true`. The delegated `$ref` was wrapped in
`{ ...get<X>Mock() }`, but the factory returns a primitive union
(e.g. number | string), so spreading it is invalid TypeScript (TS2698) and
yields `{}` at runtime. The generated dictionary value should be the bare
`get<X>Mock()` call instead.
version: 1.0.0
paths:
/integer-map:
get:
operationId: getIntegerMap
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/StringToIntegerMap'
/number-map:
get:
operationId: getNumberMap
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/StringToNumberMap'
components:
schemas:
# oneOf of primitives -> `number | string`
IntegerLike:
oneOf:
- type: integer
- type: string
StringToIntegerMap:
type: object
additionalProperties:
$ref: '#/components/schemas/IntegerLike'
# anyOf of primitives -> `number | string` (covers the anyOf arm too)
NumberLike:
anyOf:
- type: number
- type: string
StringToNumberMap:
type: object
additionalProperties:
$ref: '#/components/schemas/NumberLike'
Loading