Skip to content

Commit 6c4d0d6

Browse files
authored
perf(core): avoid repeated schema name scans (#3843)
1 parent 6063277 commit 6c4d0d6

3 files changed

Lines changed: 135 additions & 5 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { createTestContextSpec } from '../test-utils/context';
4+
import type { OpenApiSchemaObject } from '../types';
5+
import { getObject } from './object';
6+
7+
describe('getObject', () => {
8+
it('suffixes inline object property schema names that collide with component schemas', () => {
9+
const context = createTestContextSpec({
10+
spec: {
11+
components: {
12+
schemas: {
13+
UserDetails: { type: 'object' },
14+
},
15+
},
16+
},
17+
});
18+
19+
const result = getObject({
20+
item: {
21+
type: 'object',
22+
properties: {
23+
details: {
24+
type: 'object',
25+
properties: {
26+
name: { type: 'string' },
27+
},
28+
},
29+
},
30+
},
31+
name: 'User',
32+
context,
33+
nullable: '',
34+
});
35+
36+
expect(result.value).toContain('details?: UserDetailsProperty');
37+
expect(result.schemas).toEqual(
38+
expect.arrayContaining([
39+
expect.objectContaining({ name: 'UserDetailsProperty' }),
40+
]),
41+
);
42+
});
43+
44+
it('uses the same PascalCase collision semantics for differently cased or separated schema names', () => {
45+
const context = createTestContextSpec({
46+
spec: {
47+
components: {
48+
schemas: {
49+
'user-details': { type: 'object' },
50+
user_details: { type: 'object' },
51+
},
52+
},
53+
},
54+
});
55+
56+
const result = getObject({
57+
item: {
58+
type: 'object',
59+
properties: {
60+
details: {
61+
type: 'object',
62+
properties: {
63+
enabled: { type: 'boolean' },
64+
},
65+
},
66+
},
67+
} satisfies OpenApiSchemaObject,
68+
name: 'User',
69+
context,
70+
nullable: '',
71+
});
72+
73+
expect(result.value).toContain('details?: UserDetailsProperty');
74+
expect(result.schemas).toEqual(
75+
expect.arrayContaining([
76+
expect.objectContaining({ name: 'UserDetailsProperty' }),
77+
]),
78+
);
79+
});
80+
81+
it('keeps inline object property schema names unchanged when component schemas do not collide', () => {
82+
const context = createTestContextSpec({
83+
spec: {
84+
components: {
85+
schemas: {
86+
AccountDetails: { type: 'object' },
87+
},
88+
},
89+
},
90+
});
91+
92+
const result = getObject({
93+
item: {
94+
type: 'object',
95+
properties: {
96+
details: {
97+
type: 'object',
98+
properties: {
99+
name: { type: 'string' },
100+
},
101+
},
102+
},
103+
},
104+
name: 'User',
105+
context,
106+
nullable: '',
107+
});
108+
109+
expect(result.value).toContain('details?: UserDetails');
110+
expect(result.value).not.toContain('UserDetailsProperty');
111+
expect(result.schemas).toEqual(
112+
expect.arrayContaining([
113+
expect.objectContaining({ name: 'UserDetails' }),
114+
]),
115+
);
116+
});
117+
});

packages/core/src/getters/object.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ interface GetObjectOptions {
171171
formDataContext?: FormDataContext;
172172
}
173173

174+
function getNormalizedComponentSchemaNames(context: ContextSpec): Set<string> {
175+
context.normalizedComponentSchemaNames ??= new Set(
176+
Object.keys(context.spec.components?.schemas ?? {}).map((schemaName) =>
177+
pascal(schemaName),
178+
),
179+
);
180+
181+
return context.normalizedComponentSchemaNames;
182+
}
183+
174184
/**
175185
* Return the output type from an object
176186
*
@@ -398,11 +408,8 @@ export function getObject({
398408
);
399409
}
400410

401-
const allSpecSchemas = context.spec.components?.schemas ?? {};
402-
403-
const isNameAlreadyTaken = Object.keys(allSpecSchemas).some(
404-
(schemaName) => pascal(schemaName) === propName,
405-
);
411+
const isNameAlreadyTaken =
412+
getNormalizedComponentSchemaNames(context).has(propName);
406413

407414
if (isNameAlreadyTaken) {
408415
propName = propName + 'Property';

packages/core/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1387,6 +1387,12 @@ export interface ContextSpec {
13871387
* Populated by `getDynamicAnchorIndex` in `resolvers/ref.ts`.
13881388
*/
13891389
dynamicAnchorIndex?: Map<string, DynamicAnchorIndexEntry>;
1390+
/**
1391+
* Lazily-built set of normalized component schema names, used while naming
1392+
* inline object-property schemas so component name collision checks do not
1393+
* rescan `components.schemas` for every property.
1394+
*/
1395+
normalizedComponentSchemaNames?: Set<string>;
13901396
/**
13911397
* Tracks array-item mock factory names already emitted per output file scope.
13921398
* Populated by `@orval/mock` when `arrayItems: true` so shared `$ref` item

0 commit comments

Comments
 (0)