fork of hey-api/openapi-ts because I need some additional things
1import { getIrPointerPriority, matchIrPointerToGroup, preferGroups } from '../../ir/graph';
2import { buildGraph } from '../../openApi/shared/utils/graph';
3import type { Graph } from '../types/graph';
4import { walk } from '../walk';
5
6const loggerStub = {
7 timeEvent: () => ({ timeEnd: () => {} }),
8} as any;
9
10describe('walkTopological', () => {
11 const makeGraph = (deps: Array<[string, Array<string>]>, nodes: Array<string>) => {
12 const nodeDependencies = new Map<string, Set<string>>();
13 const subtreeDependencies = new Map<string, Set<string>>();
14 const reverseNodeDependencies = new Map<string, Set<string>>();
15 const nodesMap = new Map<string, any>();
16
17 for (const name of nodes) {
18 nodesMap.set(name, { key: null, node: {}, parentPointer: null });
19 }
20
21 for (const [from, toList] of deps) {
22 const s = new Set<string>(toList);
23 nodeDependencies.set(from, s);
24 subtreeDependencies.set(from, new Set<string>(toList));
25 for (const to of toList) {
26 if (!reverseNodeDependencies.has(to)) reverseNodeDependencies.set(to, new Set());
27 reverseNodeDependencies.get(to)!.add(from);
28 }
29 }
30
31 return {
32 nodeDependencies,
33 nodes: nodesMap,
34 reverseNodeDependencies,
35 subtreeDependencies,
36 transitiveDependencies: new Map<string, Set<string>>(),
37 } as unknown as Graph;
38 };
39
40 it('walks nodes in topological order for a simple acyclic graph', () => {
41 // Graph: A -> B -> C
42 const graph = makeGraph(
43 [
44 ['A', ['B']],
45 ['B', ['C']],
46 ],
47 ['A', 'B', 'C'],
48 );
49 const order: Array<string> = [];
50 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
51 expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
52 expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
53 expect(order).toHaveLength(3);
54 });
55
56 it('walks nodes in topological order for multiple roots', () => {
57 // Graph: A -> B, C -> D
58 const graph = makeGraph(
59 [
60 ['A', ['B']],
61 ['C', ['D']],
62 ],
63 ['A', 'B', 'C', 'D'],
64 );
65 const order: Array<string> = [];
66 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
67 expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
68 expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
69 expect(order).toHaveLength(4);
70 });
71
72 it('walks nodes in topological order for a disconnected graph', () => {
73 // Graph: A -> B, C (no deps), D (no deps)
74 const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']);
75 const order: Array<string> = [];
76 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
77 expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
78 expect(order).toHaveLength(4);
79 expect(order).toContain('C');
80 expect(order).toContain('D');
81 });
82
83 it('walks nodes in topological order for a diamond dependency', () => {
84 // Graph: A
85 // / \
86 // B C
87 // \ /
88 // D
89 const graph = makeGraph(
90 [
91 ['A', ['B', 'C']],
92 ['B', ['D']],
93 ['C', ['D']],
94 ],
95 ['A', 'B', 'C', 'D'],
96 );
97 const order: Array<string> = [];
98 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
99 expect(order.indexOf('D')).toBeLessThan(order.indexOf('B'));
100 expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
101 expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
102 expect(order.indexOf('C')).toBeLessThan(order.indexOf('A'));
103 expect(order).toHaveLength(4);
104 });
105
106 it('walks nodes in topological order for a long chain', () => {
107 // Graph: A -> B -> C -> D -> E
108 const graph = makeGraph(
109 [
110 ['A', ['B']],
111 ['B', ['C']],
112 ['C', ['D']],
113 ['D', ['E']],
114 ],
115 ['A', 'B', 'C', 'D', 'E'],
116 );
117 const order: Array<string> = [];
118 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
119 expect(order.indexOf('E')).toBeLessThan(order.indexOf('D'));
120 expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
121 expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
122 expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
123 expect(order).toHaveLength(5);
124 });
125
126 it('walks all nodes, including cycles', () => {
127 // Graph: A <-> B (cycle), C (no deps)
128 const graph = makeGraph(
129 [
130 ['A', ['B']],
131 ['B', ['A']],
132 ],
133 ['A', 'B', 'C'],
134 );
135 const order: Array<string> = [];
136 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
137 expect(order.sort()).toEqual(['A', 'B', 'C']);
138 });
139
140 it('matches ordering for validators-circular-ref spec', async () => {
141 const specModule = await import('../../../../../specs/3.1.x/validators-circular-ref.json');
142 const spec = specModule.default ?? specModule;
143 const { graph } = buildGraph(spec, loggerStub);
144
145 const order: Array<string> = [];
146 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
147
148 const foo = '#/components/schemas/Foo';
149 const bar = '#/components/schemas/Bar';
150 const baz = '#/components/schemas/Baz';
151 const qux = '#/components/schemas/Qux';
152
153 // Bar should come before Foo because Foo depends on Bar
154 expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo));
155
156 // Baz and Qux form a mutual $ref cycle; both must be present
157 expect(order).toContain(baz);
158 expect(order).toContain(qux);
159 });
160
161 it('prefers schema group before parameter when safe (default)', () => {
162 // parameter then schema in declaration order, no deps -> schema should move before parameter
163 const param = '#/components/parameters/P';
164 const schema = '#/components/schemas/A';
165 const nodes = [param, schema];
166 const graph = makeGraph([], nodes);
167
168 const order: Array<string> = [];
169 walk(graph, (pointer) => order.push(pointer), {
170 getPointerPriority: getIrPointerPriority,
171 matchPointerToGroup: matchIrPointerToGroup,
172 order: 'topological',
173 preferGroups,
174 });
175 expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param));
176 });
177
178 it('does not apply preferGroups when it would violate dependencies (fallback)', () => {
179 // declaration order: param, schema; schema depends on param -> cannot move before param
180 const param = '#/components/parameters/P';
181 const schema = '#/components/schemas/S';
182 const nodes = [param, schema];
183 const nodeDependencies = new Map<string, Set<string>>();
184 nodeDependencies.set(schema, new Set([param]));
185 const subtreeDependencies = new Map<string, Set<string>>();
186 const reverseNodeDependencies = new Map<string, Set<string>>();
187 const nodesMap = new Map<string, any>();
188 for (const n of nodes) nodesMap.set(n, { key: null, node: {}, parentPointer: null });
189 const graph = {
190 nodeDependencies,
191 nodes: nodesMap,
192 reverseNodeDependencies,
193 subtreeDependencies,
194 transitiveDependencies: new Map<string, Set<string>>(),
195 } as unknown as Graph;
196
197 const order: Array<string> = [];
198 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
199 // schema depends on param so param must remain before schema
200 expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema));
201 });
202
203 it('ignores self-dependencies when ordering', () => {
204 // Foo has self-ref only, Bar references Foo -> Foo should come before Bar
205 const foo = '#/components/schemas/Foo';
206 const bar = '#/components/schemas/Bar';
207 const nodes = [foo, bar];
208 const nodeDependencies = new Map<string, Set<string>>();
209 nodeDependencies.set(foo, new Set([foo]));
210 nodeDependencies.set(bar, new Set([foo]));
211
212 const nodesMap = new Map<string, any>();
213 for (const n of nodes) nodesMap.set(n, { key: null, node: {}, parentPointer: null });
214
215 const graph = {
216 nodeDependencies,
217 nodes: nodesMap,
218 reverseNodeDependencies: new Map<string, Set<string>>(),
219 subtreeDependencies: new Map<string, Set<string>>(),
220 transitiveDependencies: new Map<string, Set<string>>(),
221 } as unknown as Graph;
222
223 const order: Array<string> = [];
224 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
225 // Foo is a dependency of Bar, so Foo should come before Bar
226 expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar));
227 });
228
229 it('uses subtreeDependencies when nodeDependencies are absent', () => {
230 const parent = '#/components/schemas/Parent';
231 const child = '#/components/schemas/Child';
232 const nodes = [parent, child];
233 const nodeDependencies = new Map<string, Set<string>>();
234 const subtreeDependencies = new Map<string, Set<string>>();
235 subtreeDependencies.set(parent, new Set([child]));
236
237 const nodesMap = new Map<string, any>();
238 for (const n of nodes) nodesMap.set(n, { key: null, node: {}, parentPointer: null });
239
240 const graph = {
241 nodeDependencies,
242 nodes: nodesMap,
243 reverseNodeDependencies: new Map<string, Set<string>>(),
244 subtreeDependencies,
245 transitiveDependencies: new Map<string, Set<string>>(),
246 } as unknown as Graph;
247
248 const order: Array<string> = [];
249 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
250 expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent));
251 });
252
253 it('preserves declaration order for equal-priority items (stability)', () => {
254 const a = '#/components/schemas/A';
255 const b = '#/components/schemas/B';
256 const c = '#/components/schemas/C';
257 const nodes = [a, b, c];
258 const graph = makeGraph([], nodes);
259 const order: Array<string> = [];
260 walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
261 expect(order).toEqual(nodes);
262 });
263
264 it('walks nodes in declaration order when order=declarations', () => {
265 const a = '#/components/schemas/A';
266 const b = '#/components/schemas/B';
267 const c = '#/components/schemas/C';
268 const nodes = [a, b, c];
269 const graph = makeGraph([], nodes);
270 const order: Array<string> = [];
271 walk(graph, (pointer) => order.push(pointer), { order: 'declarations' });
272 expect(order).toEqual(nodes);
273 });
274
275 it('applies preferGroups ordering in declaration mode', () => {
276 const param = '#/components/parameters/P';
277 const schema = '#/components/schemas/A';
278 const nodes = [param, schema];
279 const graph = makeGraph([], nodes);
280
281 const order: Array<string> = [];
282 walk(graph, (pointer) => order.push(pointer), {
283 matchPointerToGroup: matchIrPointerToGroup,
284 order: 'declarations',
285 preferGroups,
286 });
287 // preferGroups puts schema before parameter
288 expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param));
289 });
290});