fork of hey-api/openapi-ts because I need some additional things
at feat/skip-token 290 lines 11 kB view raw
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});