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
81 changes: 81 additions & 0 deletions packages/mock/src/faker/getters/scalar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,3 +661,84 @@ describe('getMockScalar (post-upgrader OAS 3.0 example handling)', () => {
expect(result.value).toBe('"relaxation"');
});
});

describe('getMockScalar (array items $ref extraction and recursion guard)', () => {
const baseArg = {
imports: [],
operationId: 'test-operation',
tags: [],
splitMockImplementations: [],
existingReferencedProperties: ['Foo'],
context: { output: { override: {} } } as ContextSpec,
};

it('returns [] when items.$ref is a circular reference', () => {
const result = getMockScalar({
...baseArg,
item: {
type: 'array' as const,
name: 'test-item',
items: { $ref: '#/components/schemas/Foo' },
},
});

expect(result.value).toBe('[]');
});

it('returns [] when items is allOf with a single circular $ref', () => {
const result = getMockScalar({
...baseArg,
item: {
type: 'array' as const,
name: 'test-item',
items: { allOf: [{ $ref: '#/components/schemas/Foo' }] },
},
});

expect(result.value).toBe('[]');
});

it('returns [] when items is oneOf with a single circular $ref', () => {
const result = getMockScalar({
...baseArg,
item: {
type: 'array' as const,
name: 'test-item',
items: { oneOf: [{ $ref: '#/components/schemas/Foo' }] },
},
});

expect(result.value).toBe('[]');
});

it('returns [] when items is anyOf with a single circular $ref', () => {
const result = getMockScalar({
...baseArg,
item: {
type: 'array' as const,
name: 'test-item',
items: { anyOf: [{ $ref: '#/components/schemas/Foo' }] },
},
});

expect(result.value).toBe('[]');
});

it('does not short-circuit for multi-element allOf even if one matches a visited ref', () => {
const result = getMockScalar({
...baseArg,
item: {
type: 'array' as const,
name: 'test-item',
items: {
allOf: [
{ $ref: '#/components/schemas/Foo' },
{ $ref: '#/components/schemas/Bar' },
],
},
},
});

expect(result.value).not.toBe('[]');
});
});
40 changes: 35 additions & 5 deletions packages/mock/src/faker/getters/scalar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
Expand All @@ -9,14 +9,15 @@ import {
EnumGeneration,
escape,
type GeneratorImport,
isReference,
isString,
mergeDeep,
type MockOptions,
type OpenApiSchemaObject,
pascal,
} from '@orval/core';

import type { MockDefinition, MockSchemaObject } from '../../types';
import type { MockDefinition, MockSchema, MockSchemaObject } from '../../types';
import { isFakerVersionV9 } from '../compatible-v9';
import { DEFAULT_FORMAT_MOCK } from '../constants';
import {
Expand Down Expand Up @@ -254,22 +255,31 @@ export function getMockScalar({
return { value: '[]', imports: [], name: item.name };
}

const itemsRef = extractItemsRef(item.items);
if (
'$ref' in item.items &&
itemsRef &&
existingReferencedProperties.includes(
pascal(item.items.$ref.split('/').pop() ?? ''),
pascal(itemsRef.split('/').pop() ?? ''),
)
) {
return { value: '[]', imports: [], name: item.name };
}

// If `items` is a single-element `allOf`/`oneOf`/`anyOf` wrapping a
// `$ref`, treat it as a direct `$ref`. This avoids double-wrapping when
// the inner schema is an enum array (whose `getEnum` already emits
// `faker.helpers.arrayElements(...)`) and keeps recursion semantics in
// line with direct-$ref items.
const resolvedItems =
itemsRef && !('$ref' in item.items) ? { $ref: itemsRef } : item.items;
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const {
value,
enums,
imports: resolvedImports,
} = resolveMockValue({
schema: {
...item.items,
...resolvedItems,
name: item.name,
path: item.path ? `${item.path}.[]` : '#.[]',
},
Expand Down Expand Up @@ -408,6 +418,26 @@ export function getMockScalar({
}
}

// Returns the $ref string from array `items` — either direct ($ref on items
// itself) or wrapped in a single-element allOf/oneOf/anyOf composition.
// Multi-element compositions return undefined to preserve combine semantics.
function extractItemsRef(items: MockSchema): string | undefined {
if (isReference(items)) {
return items.$ref;
}
for (const key of ['allOf', 'oneOf', 'anyOf'] as const) {
const composed = items[key] as MockSchema[] | undefined;
if (
Array.isArray(composed) &&
composed.length === 1 &&
isReference(composed[0])
) {
return composed[0].$ref;
}
}
return;
}

function getItemType(item: MockSchemaObject) {
if (Array.isArray(item.type) && item.type.includes('null')) {
const typesWithoutNull = item.type.filter((x) => x !== 'null');
Expand Down
Loading