Skip to content

Commit 4c9fa63

Browse files
committed
refactor(deepEqual): move deepEqual function to its own module and add comprehensive tests
1 parent 26260a0 commit 4c9fa63

File tree

3 files changed

+119
-67
lines changed

3 files changed

+119
-67
lines changed

packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts

Lines changed: 1 addition & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Config } from '../../../types/config';
22
import type { Logger } from '../../../utils/logger';
33
import { jsonPointerToPath } from '../../../utils/ref';
4+
import deepEqual from '../utils/deepEqual';
45
import { buildGraph, type Graph, type Scope } from '../utils/graph';
56
import { buildName } from '../utils/name';
67
import { deepClone } from '../utils/schema';
@@ -16,43 +17,6 @@ import {
1617
specToSchemasPointerNamespace,
1718
} from './utils';
1819

19-
/**
20-
* Deep equality for JSON-compatible values (objects, arrays, primitives).
21-
* Used to determine whether read/write pruned variants actually differ.
22-
*/
23-
const deepEqual = (a: unknown, b: unknown): boolean => {
24-
if (a === b) return true;
25-
if (a === null || b === null) return a === b;
26-
const typeA = typeof a;
27-
const typeB = typeof b;
28-
if (typeA !== typeB) return false;
29-
if (typeA !== 'object') return false;
30-
31-
// Arrays
32-
if (Array.isArray(a) || Array.isArray(b)) {
33-
if (!Array.isArray(a) || !Array.isArray(b)) return false;
34-
if (a.length !== b.length) return false;
35-
for (let i = 0; i < a.length; i++) {
36-
if (!deepEqual(a[i], b[i])) return false;
37-
}
38-
return true;
39-
}
40-
41-
// Plain objects
42-
const objA = a as Record<string, unknown>;
43-
const objB = b as Record<string, unknown>;
44-
const keysA = Object.keys(objA).sort();
45-
const keysB = Object.keys(objB).sort();
46-
if (keysA.length !== keysB.length) return false;
47-
for (let i = 0; i < keysA.length; i++) {
48-
if (keysA[i] !== keysB[i]) return false;
49-
}
50-
for (const key of keysA) {
51-
if (!deepEqual(objA[key], objB[key])) return false;
52-
}
53-
return true;
54-
};
55-
5620
type OriginalSchemas = Record<string, unknown>;
5721

5822
type SplitSchemas = {
@@ -632,36 +596,6 @@ export const updateRefsInSpec = ({
632596
} else if (key === '$ref' && typeof value === 'string') {
633597
// Prefer exact match first
634598
const map = split.mapping[value];
635-
// if (!map && value.startsWith(schemasPointerNamespace)) {
636-
// // Handle nested refs like '#/components/schemas/Foo/properties/Bar'
637-
// // by remapping the base schema pointer and preserving the suffix.
638-
// const path = jsonPointerToPath(value);
639-
// // schemasPointerNamespace ends with trailing '/', so its segments length is:
640-
// const baseSegments = schemasPointerNamespace
641-
// .split('/')
642-
// .filter(Boolean).length;
643-
// if (path.length > baseSegments) {
644-
// const baseSchemaName = path[baseSegments - 1];
645-
// const baseOriginalPointer = `${schemasPointerNamespace}${baseSchemaName}`;
646-
// map = split.mapping[baseOriginalPointer];
647-
// if (map) {
648-
// const suffixSegments = path.slice(baseSegments);
649-
// const suffix = suffixSegments.length
650-
// ? `/${suffixSegments.join('/')}`
651-
// : '';
652-
// if (map.read && (!nextContext || nextContext === 'read')) {
653-
// (node as Record<string, unknown>)[key] =
654-
// `${map.read}${suffix}`;
655-
// } else if (
656-
// map.write &&
657-
// (!nextContext || nextContext === 'write')
658-
// ) {
659-
// (node as Record<string, unknown>)[key] =
660-
// `${map.write}${suffix}`;
661-
// }
662-
// }
663-
// }
664-
// } else
665599
if (map) {
666600
if (map.read && (!nextContext || nextContext === 'read')) {
667601
(node as Record<string, unknown>)[key] = map.read;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import deepEqual from '../deepEqual';
4+
5+
describe('deepEqual', () => {
6+
const scenarios: Array<{
7+
a: unknown;
8+
b: unknown;
9+
equal: boolean;
10+
name: string;
11+
}> = [
12+
// Primitives
13+
{ a: 1, b: 1, equal: true, name: 'numbers equal' },
14+
{ a: 1, b: 2, equal: false, name: 'numbers not equal' },
15+
{ a: 'a', b: 'a', equal: true, name: 'strings equal' },
16+
{ a: 'a', b: 'b', equal: false, name: 'strings not equal' },
17+
{ a: true, b: true, equal: true, name: 'booleans equal' },
18+
{ a: true, b: false, equal: false, name: 'booleans not equal' },
19+
{ a: null, b: null, equal: true, name: 'null equal' },
20+
{ a: null, b: {}, equal: false, name: 'null vs object' },
21+
{ a: undefined, b: undefined, equal: true, name: 'undefined equal' },
22+
{ a: 1, b: '1', equal: false, name: 'number vs string' },
23+
{
24+
a: Number.NaN,
25+
b: Number.NaN,
26+
equal: false,
27+
name: 'NaN vs NaN (not equal)',
28+
},
29+
30+
// Arrays
31+
{ a: [1, 2], b: [1, 2], equal: true, name: 'arrays equal' },
32+
{ a: [1, 2], b: [2, 1], equal: false, name: 'arrays different order' },
33+
{ a: [1], b: [1, 2], equal: false, name: 'arrays different length' },
34+
{
35+
a: [{ a: 1 }, 2, [3, 4]],
36+
b: [{ a: 1 }, 2, [3, 4]],
37+
equal: true,
38+
name: 'nested arrays and objects equal',
39+
},
40+
41+
// Objects
42+
{
43+
a: { a: 1, b: 2 },
44+
b: { a: 1, b: 2 },
45+
equal: true,
46+
name: 'objects equal different key order',
47+
},
48+
{
49+
a: { a: 1 },
50+
b: { a: 1, b: 2 },
51+
equal: false,
52+
name: 'objects different keys',
53+
},
54+
{
55+
a: { a: { b: 2 } },
56+
b: { a: { b: 2 } },
57+
equal: true,
58+
name: 'objects nested equal',
59+
},
60+
{
61+
a: { a: { b: 2 } },
62+
b: { a: { b: 3 } },
63+
equal: false,
64+
name: 'objects nested not equal',
65+
},
66+
{
67+
a: { a: undefined },
68+
b: { a: undefined },
69+
equal: true,
70+
name: 'object with undefined values equal',
71+
},
72+
73+
// Mismatched types
74+
{ a: [], b: {}, equal: false, name: 'array vs object' },
75+
];
76+
77+
it.each(scenarios)('compares $name', async ({ a, b, equal }) => {
78+
expect(deepEqual(a, b)).toBe(equal);
79+
});
80+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Deep equality for JSON-compatible values (objects, arrays, primitives).
3+
* Used to determine whether read/write pruned variants actually differ.
4+
*/
5+
const deepEqual = (a: unknown, b: unknown): boolean => {
6+
if (a === b) return true;
7+
if (a === null || b === null) return a === b;
8+
const typeA = typeof a;
9+
const typeB = typeof b;
10+
if (typeA !== typeB) return false;
11+
if (typeA !== 'object') return false;
12+
13+
// Arrays
14+
if (Array.isArray(a) || Array.isArray(b)) {
15+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
16+
if (a.length !== b.length) return false;
17+
for (let i = 0; i < a.length; i++) {
18+
if (!deepEqual(a[i], b[i])) return false;
19+
}
20+
return true;
21+
}
22+
23+
// Plain objects
24+
const objA = a as Record<string, unknown>;
25+
const objB = b as Record<string, unknown>;
26+
const keysA = Object.keys(objA).sort();
27+
const keysB = Object.keys(objB).sort();
28+
if (keysA.length !== keysB.length) return false;
29+
for (let i = 0; i < keysA.length; i++) {
30+
if (keysA[i] !== keysB[i]) return false;
31+
}
32+
for (const key of keysA) {
33+
if (!deepEqual(objA[key], objB[key])) return false;
34+
}
35+
return true;
36+
};
37+
38+
export default deepEqual;

0 commit comments

Comments
 (0)