fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #2831 from hey-api/refactor/graph-declarations

refactor: add walk function instead of hard-coded logic

authored by

Lubos and committed by
GitHub
bd18df95 b2ab11a5

+688 -671
+2 -2
packages/openapi-ts-tests/main/test/openapi-ts.config.ts
··· 40 40 // 'dutchie.json', 41 41 // 'invalid', 42 42 // 'openai.yaml', 43 - // 'full.yaml', 43 + 'full.yaml', 44 44 // 'opencode.yaml', 45 45 // 'sdk-instance.yaml', 46 46 // 'string-with-format.yaml', 47 - 'transformers.json', 47 + // 'transformers.json', 48 48 // 'type-format.yaml', 49 49 // 'validators.yaml', 50 50 // 'validators-circular-ref.json',
+1 -1
packages/openapi-ts/src/debug/graph.ts
··· 1 - import type { Graph } from '~/openApi/shared/utils/graph'; 1 + import type { Graph } from '~/graph'; 2 2 3 3 const analyzeStructure = (graph: Graph) => { 4 4 let maxDepth = 0;
+306
packages/openapi-ts/src/graph/__tests__/walk.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + 3 + import type { Graph } from '~/graph'; 4 + import { 5 + getIrPointerPriority, 6 + matchIrPointerToGroup, 7 + preferGroups, 8 + } from '~/ir/graph'; 9 + import { buildGraph } from '~/openApi/shared/utils/graph'; 10 + 11 + import { walk } from '../walk'; 12 + 13 + const loggerStub = { 14 + timeEvent: () => ({ timeEnd: () => {} }), 15 + } as any; 16 + 17 + describe('walkTopological', () => { 18 + const makeGraph = ( 19 + deps: Array<[string, Array<string>]>, 20 + nodes: Array<string>, 21 + ) => { 22 + const nodeDependencies = new Map<string, Set<string>>(); 23 + const subtreeDependencies = new Map<string, Set<string>>(); 24 + const reverseNodeDependencies = new Map<string, Set<string>>(); 25 + const nodesMap = new Map<string, any>(); 26 + 27 + for (const name of nodes) { 28 + nodesMap.set(name, { key: null, node: {}, parentPointer: null }); 29 + } 30 + 31 + for (const [from, toList] of deps) { 32 + const s = new Set<string>(toList); 33 + nodeDependencies.set(from, s); 34 + subtreeDependencies.set(from, new Set<string>(toList)); 35 + for (const to of toList) { 36 + if (!reverseNodeDependencies.has(to)) 37 + reverseNodeDependencies.set(to, new Set()); 38 + reverseNodeDependencies.get(to)!.add(from); 39 + } 40 + } 41 + 42 + return { 43 + nodeDependencies, 44 + nodes: nodesMap, 45 + reverseNodeDependencies, 46 + subtreeDependencies, 47 + transitiveDependencies: new Map<string, Set<string>>(), 48 + } as unknown as Graph; 49 + }; 50 + 51 + it('walks nodes in topological order for a simple acyclic graph', () => { 52 + // Graph: A -> B -> C 53 + const graph = makeGraph( 54 + [ 55 + ['A', ['B']], 56 + ['B', ['C']], 57 + ], 58 + ['A', 'B', 'C'], 59 + ); 60 + const order: Array<string> = []; 61 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 62 + expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); 63 + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 64 + expect(order).toHaveLength(3); 65 + }); 66 + 67 + it('walks nodes in topological order for multiple roots', () => { 68 + // Graph: A -> B, C -> D 69 + const graph = makeGraph( 70 + [ 71 + ['A', ['B']], 72 + ['C', ['D']], 73 + ], 74 + ['A', 'B', 'C', 'D'], 75 + ); 76 + const order: Array<string> = []; 77 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 78 + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 79 + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); 80 + expect(order).toHaveLength(4); 81 + }); 82 + 83 + it('walks nodes in topological order for a disconnected graph', () => { 84 + // Graph: A -> B, C (no deps), D (no deps) 85 + const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']); 86 + const order: Array<string> = []; 87 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 88 + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 89 + expect(order).toHaveLength(4); 90 + expect(order).toContain('C'); 91 + expect(order).toContain('D'); 92 + }); 93 + 94 + it('walks nodes in topological order for a diamond dependency', () => { 95 + // Graph: A 96 + // / \ 97 + // B C 98 + // \ / 99 + // D 100 + const graph = makeGraph( 101 + [ 102 + ['A', ['B', 'C']], 103 + ['B', ['D']], 104 + ['C', ['D']], 105 + ], 106 + ['A', 'B', 'C', 'D'], 107 + ); 108 + const order: Array<string> = []; 109 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 110 + expect(order.indexOf('D')).toBeLessThan(order.indexOf('B')); 111 + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); 112 + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 113 + expect(order.indexOf('C')).toBeLessThan(order.indexOf('A')); 114 + expect(order).toHaveLength(4); 115 + }); 116 + 117 + it('walks nodes in topological order for a long chain', () => { 118 + // Graph: A -> B -> C -> D -> E 119 + const graph = makeGraph( 120 + [ 121 + ['A', ['B']], 122 + ['B', ['C']], 123 + ['C', ['D']], 124 + ['D', ['E']], 125 + ], 126 + ['A', 'B', 'C', 'D', 'E'], 127 + ); 128 + const order: Array<string> = []; 129 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 130 + expect(order.indexOf('E')).toBeLessThan(order.indexOf('D')); 131 + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); 132 + expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); 133 + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 134 + expect(order).toHaveLength(5); 135 + }); 136 + 137 + it('walks all nodes, including cycles', () => { 138 + // Graph: A <-> B (cycle), C (no deps) 139 + const graph = makeGraph( 140 + [ 141 + ['A', ['B']], 142 + ['B', ['A']], 143 + ], 144 + ['A', 'B', 'C'], 145 + ); 146 + const order: Array<string> = []; 147 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 148 + expect(order.sort()).toEqual(['A', 'B', 'C']); 149 + }); 150 + 151 + it('matches ordering for validators-circular-ref spec', async () => { 152 + const specModule = await import( 153 + '../../../../../specs/3.1.x/validators-circular-ref.json' 154 + ); 155 + const spec = specModule.default ?? specModule; 156 + const { graph } = buildGraph(spec, loggerStub); 157 + 158 + const order: Array<string> = []; 159 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 160 + 161 + const foo = '#/components/schemas/Foo'; 162 + const bar = '#/components/schemas/Bar'; 163 + const baz = '#/components/schemas/Baz'; 164 + const qux = '#/components/schemas/Qux'; 165 + 166 + // Bar should come before Foo because Foo depends on Bar 167 + expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo)); 168 + 169 + // Baz and Qux form a mutual $ref cycle; both must be present 170 + expect(order).toContain(baz); 171 + expect(order).toContain(qux); 172 + }); 173 + 174 + it('prefers schema group before parameter when safe (default)', () => { 175 + // parameter then schema in declaration order, no deps -> schema should move before parameter 176 + const param = '#/components/parameters/P'; 177 + const schema = '#/components/schemas/A'; 178 + const nodes = [param, schema]; 179 + const graph = makeGraph([], nodes); 180 + 181 + const order: Array<string> = []; 182 + walk(graph, (pointer) => order.push(pointer), { 183 + getPointerPriority: getIrPointerPriority, 184 + matchPointerToGroup: matchIrPointerToGroup, 185 + order: 'topological', 186 + preferGroups, 187 + }); 188 + expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param)); 189 + }); 190 + 191 + it('does not apply preferGroups when it would violate dependencies (fallback)', () => { 192 + // declaration order: param, schema; schema depends on param -> cannot move before param 193 + const param = '#/components/parameters/P'; 194 + const schema = '#/components/schemas/S'; 195 + const nodes = [param, schema]; 196 + const nodeDependencies = new Map<string, Set<string>>(); 197 + nodeDependencies.set(schema, new Set([param])); 198 + const subtreeDependencies = new Map<string, Set<string>>(); 199 + const reverseNodeDependencies = new Map<string, Set<string>>(); 200 + const nodesMap = new Map<string, any>(); 201 + for (const n of nodes) 202 + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); 203 + const graph = { 204 + nodeDependencies, 205 + nodes: nodesMap, 206 + reverseNodeDependencies, 207 + subtreeDependencies, 208 + transitiveDependencies: new Map<string, Set<string>>(), 209 + } as unknown as Graph; 210 + 211 + const order: Array<string> = []; 212 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 213 + // schema depends on param so param must remain before schema 214 + expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema)); 215 + }); 216 + 217 + it('ignores self-dependencies when ordering', () => { 218 + // Foo has self-ref only, Bar references Foo -> Foo should come before Bar 219 + const foo = '#/components/schemas/Foo'; 220 + const bar = '#/components/schemas/Bar'; 221 + const nodes = [foo, bar]; 222 + const nodeDependencies = new Map<string, Set<string>>(); 223 + nodeDependencies.set(foo, new Set([foo])); 224 + nodeDependencies.set(bar, new Set([foo])); 225 + 226 + const nodesMap = new Map<string, any>(); 227 + for (const n of nodes) 228 + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); 229 + 230 + const graph = { 231 + nodeDependencies, 232 + nodes: nodesMap, 233 + reverseNodeDependencies: new Map<string, Set<string>>(), 234 + subtreeDependencies: new Map<string, Set<string>>(), 235 + transitiveDependencies: new Map<string, Set<string>>(), 236 + } as unknown as Graph; 237 + 238 + const order: Array<string> = []; 239 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 240 + // Foo is a dependency of Bar, so Foo should come before Bar 241 + expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar)); 242 + }); 243 + 244 + it('uses subtreeDependencies when nodeDependencies are absent', () => { 245 + const parent = '#/components/schemas/Parent'; 246 + const child = '#/components/schemas/Child'; 247 + const nodes = [parent, child]; 248 + const nodeDependencies = new Map<string, Set<string>>(); 249 + const subtreeDependencies = new Map<string, Set<string>>(); 250 + subtreeDependencies.set(parent, new Set([child])); 251 + 252 + const nodesMap = new Map<string, any>(); 253 + for (const n of nodes) 254 + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); 255 + 256 + const graph = { 257 + nodeDependencies, 258 + nodes: nodesMap, 259 + reverseNodeDependencies: new Map<string, Set<string>>(), 260 + subtreeDependencies, 261 + transitiveDependencies: new Map<string, Set<string>>(), 262 + } as unknown as Graph; 263 + 264 + const order: Array<string> = []; 265 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 266 + expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent)); 267 + }); 268 + 269 + it('preserves declaration order for equal-priority items (stability)', () => { 270 + const a = '#/components/schemas/A'; 271 + const b = '#/components/schemas/B'; 272 + const c = '#/components/schemas/C'; 273 + const nodes = [a, b, c]; 274 + const graph = makeGraph([], nodes); 275 + const order: Array<string> = []; 276 + walk(graph, (pointer) => order.push(pointer), { order: 'topological' }); 277 + expect(order).toEqual(nodes); 278 + }); 279 + 280 + it('walks nodes in declaration order when order=declarations', () => { 281 + const a = '#/components/schemas/A'; 282 + const b = '#/components/schemas/B'; 283 + const c = '#/components/schemas/C'; 284 + const nodes = [a, b, c]; 285 + const graph = makeGraph([], nodes); 286 + const order: Array<string> = []; 287 + walk(graph, (pointer) => order.push(pointer), { order: 'declarations' }); 288 + expect(order).toEqual(nodes); 289 + }); 290 + 291 + it('applies preferGroups ordering in declaration mode', () => { 292 + const param = '#/components/parameters/P'; 293 + const schema = '#/components/schemas/A'; 294 + const nodes = [param, schema]; 295 + const graph = makeGraph([], nodes); 296 + 297 + const order: Array<string> = []; 298 + walk(graph, (pointer) => order.push(pointer), { 299 + matchPointerToGroup: matchIrPointerToGroup, 300 + order: 'declarations', 301 + preferGroups, 302 + }); 303 + // preferGroups puts schema before parameter 304 + expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param)); 305 + }); 306 + });
+7
packages/openapi-ts/src/graph/index.ts
··· 1 + export type { Graph, NodeInfo } from './types/graph'; 2 + export type { 3 + GetPointerPriorityFn, 4 + PointerGroupMatch, 5 + WalkOptions, 6 + } from './types/walk'; 7 + export { walk } from './walk';
+60
packages/openapi-ts/src/graph/types/graph.d.ts
··· 1 + /** 2 + * The main graph structure for OpenAPI node analysis. 3 + * 4 + * @property nodeDependencies - For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. Nodes with no dependencies are omitted. 5 + * @property nodes - Map from normalized JSON Pointer to NodeInfo for every node in the spec. 6 + * @property reverseNodeDependencies - For each node with at least one dependent, the set of nodes that reference it via $ref. Nodes with no dependents are omitted. 7 + */ 8 + export type Graph = { 9 + /** 10 + * For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. 11 + * Nodes with no dependencies are omitted from this map. 12 + */ 13 + nodeDependencies: Map<string, Set<string>>; 14 + /** 15 + * Map from normalized JSON Pointer to NodeInfo for every node in the spec. 16 + */ 17 + nodes: Map<string, NodeInfo>; 18 + /** 19 + * For each node with at least one dependent, the set of nodes that reference it via $ref. 20 + * Nodes with no dependents are omitted from this map. 21 + */ 22 + reverseNodeDependencies: Map<string, Set<string>>; 23 + /** 24 + * For each node, the set of direct $ref targets that appear anywhere inside the node's 25 + * subtree (the node itself and its children). This is populated during graph construction 26 + * and is used to compute top-level dependency relationships where $ref may be attached to 27 + * child pointers instead of the parent. 28 + */ 29 + subtreeDependencies: Map<string, Set<string>>; 30 + /** 31 + * For each node, the set of all (transitive) normalized JSON Pointers it references via $ref anywhere in its subtree. 32 + * This includes both direct and indirect dependencies, making it useful for filtering, codegen, and tree-shaking. 33 + */ 34 + transitiveDependencies: Map<string, Set<string>>; 35 + }; 36 + 37 + /** 38 + * Information about a node in the OpenAPI graph. 39 + * 40 + * @property deprecated - Whether the node is deprecated. Optional. 41 + * @property key - The property name or array index in the parent, or null for root. 42 + * @property node - The actual object at this pointer in the spec. 43 + * @property parentPointer - The JSON Pointer of the parent node, or null for root. 44 + * @property scopes - The set of access scopes for this node, if any. Optional. 45 + * @property tags - The set of tags for this node, if any. Optional. 46 + */ 47 + export type NodeInfo = { 48 + /** Whether the node is deprecated. Optional. */ 49 + deprecated?: boolean; 50 + /** The property name or array index in the parent, or null for root. */ 51 + key: string | number | null; 52 + /** The actual object at this pointer in the spec. */ 53 + node: unknown; 54 + /** The JSON Pointer of the parent node, or null for root. */ 55 + parentPointer: string | null; 56 + /** The set of access scopes for this node, if any. Optional. */ 57 + scopes?: Set<Scope>; 58 + /** The set of tags for this node, if any. Optional. */ 59 + tags?: Set<string>; 60 + };
+54
packages/openapi-ts/src/graph/types/walk.d.ts
··· 1 + import type { Graph, NodeInfo } from './graph'; 2 + 3 + export type WalkCallbackFn = (pointer: string, nodeInfo: NodeInfo) => void; 4 + 5 + export type GetPointerPriorityFn = (pointer: string) => number; 6 + 7 + export type PointerGroupMatch<T extends string = string> = 8 + | { kind: T; matched: true } 9 + | { kind?: undefined; matched: false }; 10 + 11 + export type WalkOptions<T extends string = string> = { 12 + /** 13 + * Optional priority function used to compute a numeric priority for each 14 + * pointer. Lower values are emitted earlier. Useful to customize ordering 15 + * beyond the built-in group preferences. 16 + */ 17 + getPointerPriority?: GetPointerPriorityFn; 18 + /** 19 + * Optional function to match a pointer to a group name. 20 + * 21 + * @param pointer The pointer string 22 + * @returns The group name, or undefined if no match 23 + */ 24 + matchPointerToGroup?: (pointer: string) => PointerGroupMatch<T>; 25 + /** 26 + * Order of walking schemas. 27 + * 28 + * The "declarations" option ensures that schemas are walked in the order 29 + * they are declared in the input document. This is useful for scenarios where 30 + * the order of declaration matters, such as when generating code that relies 31 + * on the sequence of schema definitions. 32 + * 33 + * The "topological" option ensures that schemas are walked in an order 34 + * where dependencies are visited before the schemas that depend on them. 35 + * This is useful for scenarios where you need to process or generate 36 + * schemas in a way that respects their interdependencies. 37 + * 38 + * @default 'topological' 39 + */ 40 + order?: 'declarations' | 'topological'; 41 + /** 42 + * Optional grouping preference for walking. When provided, walk function 43 + * will prefer emitting kinds listed earlier in this array when it is safe 44 + * to do so (it will only apply the preference when doing so does not 45 + * violate dependency ordering). 46 + */ 47 + preferGroups?: ReadonlyArray<T>; 48 + }; 49 + 50 + export type WalkFn = ( 51 + graph: Graph, 52 + callback: WalkCallbackFn, 53 + options?: WalkOptions, 54 + ) => void;
+208
packages/openapi-ts/src/graph/walk.ts
··· 1 + import { MinHeap } from '~/utils/minHeap'; 2 + 3 + import type { GetPointerPriorityFn, WalkFn } from './types/walk'; 4 + 5 + /** 6 + * Walk the nodes of the graph in declaration (insertion) order. 7 + * This is a cheap alternative to `walkTopological` when dependency ordering 8 + * is not required and the caller only wants nodes in the order they were 9 + * added to the graph. 10 + */ 11 + const walkDeclarations: WalkFn = (graph, callback, options) => { 12 + const pointers = Array.from(graph.nodes.keys()); 13 + 14 + if (options?.preferGroups && options.preferGroups.length > 0) { 15 + // emit nodes that match each preferred group in order, preserving insertion order 16 + const emitted = new Set<string>(); 17 + if (options.matchPointerToGroup) { 18 + for (const kind of options.preferGroups) { 19 + for (const pointer of pointers) { 20 + const result = options.matchPointerToGroup(pointer); 21 + if (!result.matched) continue; 22 + if (result.kind === kind) { 23 + emitted.add(pointer); 24 + callback(pointer, graph.nodes.get(pointer)!); 25 + } 26 + } 27 + } 28 + } 29 + 30 + // emit anything not covered by the preferGroups (in declaration order) 31 + for (const pointer of pointers) { 32 + if (emitted.has(pointer)) continue; 33 + callback(pointer, graph.nodes.get(pointer)!); 34 + } 35 + return; 36 + } 37 + 38 + // fallback: simple declaration order 39 + for (const pointer of pointers) { 40 + callback(pointer, graph.nodes.get(pointer)!); 41 + } 42 + }; 43 + 44 + /** 45 + * Walks the nodes of the graph in topological order (dependencies before dependents). 46 + * Calls the callback for each node pointer in order. 47 + * Nodes in cycles are grouped together and emitted in arbitrary order within the group. 48 + * 49 + * @param graph - The dependency graph 50 + * @param callback - Function to call for each node pointer 51 + */ 52 + const walkTopological: WalkFn = (graph, callback, options) => { 53 + // stable Kahn's algorithm that respects declaration order as a tiebreaker. 54 + const pointers = Array.from(graph.nodes.keys()); 55 + // base insertion order 56 + const baseIndex = new Map<string, number>(); 57 + pointers.forEach((pointer, index) => baseIndex.set(pointer, index)); 58 + 59 + // composite decl index: group priority then base insertion order 60 + const declIndex = new Map<string, number>(); 61 + for (const pointer of pointers) { 62 + const priority = options?.getPointerPriority?.(pointer) ?? 10; 63 + const composite = priority * 1_000_000 + (baseIndex.get(pointer) ?? 0); 64 + declIndex.set(pointer, composite); 65 + } 66 + 67 + // build dependency sets for each pointer 68 + const depsOf = new Map<string, Set<string>>(); 69 + for (const pointer of pointers) { 70 + const raw = graph.subtreeDependencies?.get(pointer) ?? new Set(); 71 + const filtered = new Set<string>(); 72 + for (const rawPointer of raw) { 73 + if (rawPointer === pointer) continue; // ignore self-dependencies for ordering 74 + if (graph.nodes.has(rawPointer)) { 75 + filtered.add(rawPointer); 76 + } 77 + } 78 + depsOf.set(pointer, filtered); 79 + } 80 + 81 + // build inDegree and dependents adjacency 82 + const inDegree = new Map<string, number>(); 83 + const dependents = new Map<string, Set<string>>(); 84 + for (const pointer of pointers) { 85 + inDegree.set(pointer, 0); 86 + } 87 + for (const [pointer, deps] of depsOf) { 88 + inDegree.set(pointer, deps.size); 89 + for (const d of deps) { 90 + if (!dependents.has(d)) { 91 + dependents.set(d, new Set()); 92 + } 93 + dependents.get(d)!.add(pointer); 94 + } 95 + } 96 + 97 + // sort pointers by declaration order 98 + const sortByDecl = (arr: Array<string>) => 99 + arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!); 100 + 101 + // initialize queue with zero-inDegree nodes in declaration order 102 + // use min-heap prioritized by declaration index to avoid repeated full sorts 103 + const heap = new MinHeap(declIndex); 104 + for (const pointer of pointers) { 105 + if ((inDegree.get(pointer) ?? 0) === 0) { 106 + heap.push(pointer); 107 + } 108 + } 109 + 110 + const emitted = new Set<string>(); 111 + const order: Array<string> = []; 112 + 113 + while (!heap.isEmpty()) { 114 + const cur = heap.pop()!; 115 + if (emitted.has(cur)) continue; 116 + emitted.add(cur); 117 + order.push(cur); 118 + 119 + const deps = dependents.get(cur); 120 + if (!deps) continue; 121 + 122 + for (const dep of deps) { 123 + const v = (inDegree.get(dep) ?? 0) - 1; 124 + inDegree.set(dep, v); 125 + if (v === 0) { 126 + heap.push(dep); 127 + } 128 + } 129 + } 130 + 131 + // emit remaining nodes (cycles) in declaration order 132 + const remaining = pointers.filter((pointer) => !emitted.has(pointer)); 133 + sortByDecl(remaining); 134 + for (const pointer of remaining) { 135 + emitted.add(pointer); 136 + order.push(pointer); 137 + } 138 + 139 + // prefer specified groups when safe 140 + let finalOrder = order; 141 + if (options?.preferGroups && options.preferGroups.length > 0) { 142 + // build group priority map (lower = earlier) 143 + const groupPriority = new Map<string, number>(); 144 + for (let i = 0; i < options.preferGroups.length; i++) { 145 + const k = options.preferGroups[i]; 146 + if (k) { 147 + groupPriority.set(k, i); 148 + } 149 + } 150 + 151 + const getGroup: GetPointerPriorityFn = (pointer) => { 152 + if (options.matchPointerToGroup) { 153 + const result = options.matchPointerToGroup(pointer); 154 + if (result.matched) { 155 + return groupPriority.has(result.kind) 156 + ? groupPriority.get(result.kind)! 157 + : options.preferGroups!.length; 158 + } 159 + } 160 + return options.preferGroups!.length; 161 + }; 162 + 163 + // proposed order: sort by (groupPriority, originalIndex) 164 + const proposed = [...order].sort((a, b) => { 165 + const ga = getGroup(a); 166 + const gb = getGroup(b); 167 + return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b); 168 + }); 169 + 170 + // build quick lookup of original index and proposed index 171 + const proposedIndex = new Map<string, number>(); 172 + for (let i = 0; i < proposed.length; i++) { 173 + proposedIndex.set(proposed[i]!, i); 174 + } 175 + 176 + // only validate edges where group(dep) > group(node) 177 + const violated = (() => { 178 + for (const [node, deps] of depsOf) { 179 + for (const dep of deps) { 180 + const gDep = getGroup(dep); 181 + const gNode = getGroup(node); 182 + if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping 183 + const pDep = proposedIndex.get(dep)!; 184 + const pNode = proposedIndex.get(node)!; 185 + if (pDep >= pNode) { 186 + return true; 187 + } 188 + } 189 + } 190 + return false; 191 + })(); 192 + 193 + if (!violated) { 194 + finalOrder = proposed; 195 + } 196 + } 197 + 198 + for (const pointer of finalOrder) { 199 + callback(pointer, graph.nodes.get(pointer)!); 200 + } 201 + }; 202 + 203 + export const walk: WalkFn = (graph, callback, options) => { 204 + if (options?.order === 'topological') { 205 + return walkTopological(graph, callback, options); 206 + } 207 + return walkDeclarations(graph, callback, options); 208 + };
+3 -270
packages/openapi-ts/src/ir/__tests__/graph.test.ts
··· 1 1 import { describe, expect, it } from 'vitest'; 2 2 3 - import type { Graph } from '~/openApi/shared/utils/graph'; 4 - import { buildGraph } from '~/openApi/shared/utils/graph'; 5 - 6 3 import type { IrTopLevelKind } from '../graph'; 7 - import { matchIrTopLevelPointer, walkTopological } from '../graph'; 8 - 9 - // simple logger stub for buildGraph 10 - const loggerStub = { 11 - timeEvent: () => ({ timeEnd: () => {} }), 12 - } as any; 4 + import { matchIrPointerToGroup } from '../graph'; 13 5 14 - describe('matchIrTopLevelPointer', () => { 6 + describe('matchIrPointerToGroup', () => { 15 7 const cases: Array< 16 8 [ 17 9 string, ··· 56 48 57 49 for (const [pointer, kind, expected] of cases) { 58 50 it(`matches ${pointer} with kind=${kind}`, () => { 59 - const result = matchIrTopLevelPointer(pointer, kind as IrTopLevelKind); 51 + const result = matchIrPointerToGroup(pointer, kind as IrTopLevelKind); 60 52 expect(result.matched).toBe(expected.matched); 61 53 if (expected.matched) { 62 54 expect(result.kind).toBe(expected.kind); ··· 66 58 }); 67 59 } 68 60 }); 69 - 70 - describe('walkTopological', () => { 71 - const makeGraph = ( 72 - deps: Array<[string, Array<string>]>, 73 - nodes: Array<string>, 74 - ) => { 75 - const nodeDependencies = new Map<string, Set<string>>(); 76 - const subtreeDependencies = new Map<string, Set<string>>(); 77 - const reverseNodeDependencies = new Map<string, Set<string>>(); 78 - const nodesMap = new Map<string, any>(); 79 - 80 - for (const name of nodes) { 81 - nodesMap.set(name, { key: null, node: {}, parentPointer: null }); 82 - } 83 - 84 - for (const [from, toList] of deps) { 85 - const s = new Set<string>(toList); 86 - nodeDependencies.set(from, s); 87 - subtreeDependencies.set(from, new Set<string>(toList)); 88 - for (const to of toList) { 89 - if (!reverseNodeDependencies.has(to)) 90 - reverseNodeDependencies.set(to, new Set()); 91 - reverseNodeDependencies.get(to)!.add(from); 92 - } 93 - } 94 - 95 - return { 96 - nodeDependencies, 97 - nodes: nodesMap, 98 - reverseNodeDependencies, 99 - subtreeDependencies, 100 - transitiveDependencies: new Map<string, Set<string>>(), 101 - } as unknown as Graph; 102 - }; 103 - 104 - it('walks nodes in topological order for a simple acyclic graph', () => { 105 - // Graph: A -> B -> C 106 - const graph = makeGraph( 107 - [ 108 - ['A', ['B']], 109 - ['B', ['C']], 110 - ], 111 - ['A', 'B', 'C'], 112 - ); 113 - const order: Array<string> = []; 114 - walkTopological(graph, (pointer) => order.push(pointer)); 115 - expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); 116 - expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 117 - expect(order).toHaveLength(3); 118 - }); 119 - 120 - it('walks nodes in topological order for multiple roots', () => { 121 - // Graph: A -> B, C -> D 122 - const graph = makeGraph( 123 - [ 124 - ['A', ['B']], 125 - ['C', ['D']], 126 - ], 127 - ['A', 'B', 'C', 'D'], 128 - ); 129 - const order: Array<string> = []; 130 - walkTopological(graph, (pointer) => order.push(pointer)); 131 - expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 132 - expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); 133 - expect(order).toHaveLength(4); 134 - }); 135 - 136 - it('walks nodes in topological order for a disconnected graph', () => { 137 - // Graph: A -> B, C (no deps), D (no deps) 138 - const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']); 139 - const order: Array<string> = []; 140 - walkTopological(graph, (pointer) => order.push(pointer)); 141 - expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 142 - expect(order).toHaveLength(4); 143 - expect(order).toContain('C'); 144 - expect(order).toContain('D'); 145 - }); 146 - 147 - it('walks nodes in topological order for a diamond dependency', () => { 148 - // Graph: A 149 - // / \ 150 - // B C 151 - // \ / 152 - // D 153 - const graph = makeGraph( 154 - [ 155 - ['A', ['B', 'C']], 156 - ['B', ['D']], 157 - ['C', ['D']], 158 - ], 159 - ['A', 'B', 'C', 'D'], 160 - ); 161 - const order: Array<string> = []; 162 - walkTopological(graph, (pointer) => order.push(pointer)); 163 - expect(order.indexOf('D')).toBeLessThan(order.indexOf('B')); 164 - expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); 165 - expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 166 - expect(order.indexOf('C')).toBeLessThan(order.indexOf('A')); 167 - expect(order).toHaveLength(4); 168 - }); 169 - 170 - it('walks nodes in topological order for a long chain', () => { 171 - // Graph: A -> B -> C -> D -> E 172 - const graph = makeGraph( 173 - [ 174 - ['A', ['B']], 175 - ['B', ['C']], 176 - ['C', ['D']], 177 - ['D', ['E']], 178 - ], 179 - ['A', 'B', 'C', 'D', 'E'], 180 - ); 181 - const order: Array<string> = []; 182 - walkTopological(graph, (pointer) => order.push(pointer)); 183 - expect(order.indexOf('E')).toBeLessThan(order.indexOf('D')); 184 - expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); 185 - expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); 186 - expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); 187 - expect(order).toHaveLength(5); 188 - }); 189 - 190 - it('walks all nodes, including cycles', () => { 191 - // Graph: A <-> B (cycle), C (no deps) 192 - const graph = makeGraph( 193 - [ 194 - ['A', ['B']], 195 - ['B', ['A']], 196 - ], 197 - ['A', 'B', 'C'], 198 - ); 199 - const order: Array<string> = []; 200 - walkTopological(graph, (pointer) => order.push(pointer)); 201 - expect(order.sort()).toEqual(['A', 'B', 'C']); 202 - }); 203 - 204 - it('matches ordering for validators-circular-ref spec', async () => { 205 - const specModule = await import( 206 - '../../../../../specs/3.1.x/validators-circular-ref.json' 207 - ); 208 - const spec = specModule.default ?? specModule; 209 - const { graph } = buildGraph(spec, loggerStub); 210 - 211 - const order: Array<string> = []; 212 - walkTopological(graph, (pointer) => order.push(pointer)); 213 - 214 - const foo = '#/components/schemas/Foo'; 215 - const bar = '#/components/schemas/Bar'; 216 - const baz = '#/components/schemas/Baz'; 217 - const qux = '#/components/schemas/Qux'; 218 - 219 - // Bar should come before Foo because Foo depends on Bar 220 - expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo)); 221 - 222 - // Baz and Qux form a mutual $ref cycle; both must be present 223 - expect(order).toContain(baz); 224 - expect(order).toContain(qux); 225 - }); 226 - 227 - it('prefers schema group before parameter when safe (default)', () => { 228 - // parameter then schema in declaration order, no deps -> schema should move before parameter 229 - const param = '#/components/parameters/P'; 230 - const schema = '#/components/schemas/A'; 231 - const nodes = [param, schema]; 232 - const graph = makeGraph([], nodes); 233 - 234 - const order: Array<string> = []; 235 - walkTopological(graph, (p) => order.push(p)); 236 - expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param)); 237 - }); 238 - 239 - it('does not apply preferGroups when it would violate dependencies (fallback)', () => { 240 - // declaration order: param, schema; schema depends on param -> cannot move before param 241 - const param = '#/components/parameters/P'; 242 - const schema = '#/components/schemas/S'; 243 - const nodes = [param, schema]; 244 - const nodeDependencies = new Map<string, Set<string>>(); 245 - nodeDependencies.set(schema, new Set([param])); 246 - const subtreeDependencies = new Map<string, Set<string>>(); 247 - const reverseNodeDependencies = new Map<string, Set<string>>(); 248 - const nodesMap = new Map<string, any>(); 249 - for (const n of nodes) 250 - nodesMap.set(n, { key: null, node: {}, parentPointer: null }); 251 - const graph = { 252 - nodeDependencies, 253 - nodes: nodesMap, 254 - reverseNodeDependencies, 255 - subtreeDependencies, 256 - transitiveDependencies: new Map<string, Set<string>>(), 257 - } as unknown as Graph; 258 - 259 - const order: Array<string> = []; 260 - walkTopological(graph, (p) => order.push(p)); 261 - // schema depends on param so param must remain before schema 262 - expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema)); 263 - }); 264 - 265 - it('ignores self-dependencies when ordering', () => { 266 - // Foo has self-ref only, Bar references Foo -> Foo should come before Bar 267 - const foo = '#/components/schemas/Foo'; 268 - const bar = '#/components/schemas/Bar'; 269 - const nodes = [foo, bar]; 270 - const nodeDependencies = new Map<string, Set<string>>(); 271 - nodeDependencies.set(foo, new Set([foo])); 272 - nodeDependencies.set(bar, new Set([foo])); 273 - 274 - const nodesMap = new Map<string, any>(); 275 - for (const n of nodes) 276 - nodesMap.set(n, { key: null, node: {}, parentPointer: null }); 277 - 278 - const graph = { 279 - nodeDependencies, 280 - nodes: nodesMap, 281 - reverseNodeDependencies: new Map<string, Set<string>>(), 282 - subtreeDependencies: new Map<string, Set<string>>(), 283 - transitiveDependencies: new Map<string, Set<string>>(), 284 - } as unknown as Graph; 285 - 286 - const order: Array<string> = []; 287 - walkTopological(graph, (p) => order.push(p)); 288 - // Foo is a dependency of Bar, so Foo should come before Bar 289 - expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar)); 290 - }); 291 - 292 - it('uses subtreeDependencies when nodeDependencies are absent', () => { 293 - const parent = '#/components/schemas/Parent'; 294 - const child = '#/components/schemas/Child'; 295 - const nodes = [parent, child]; 296 - const nodeDependencies = new Map<string, Set<string>>(); 297 - const subtreeDependencies = new Map<string, Set<string>>(); 298 - subtreeDependencies.set(parent, new Set([child])); 299 - 300 - const nodesMap = new Map<string, any>(); 301 - for (const n of nodes) 302 - nodesMap.set(n, { key: null, node: {}, parentPointer: null }); 303 - 304 - const graph = { 305 - nodeDependencies, 306 - nodes: nodesMap, 307 - reverseNodeDependencies: new Map<string, Set<string>>(), 308 - subtreeDependencies, 309 - transitiveDependencies: new Map<string, Set<string>>(), 310 - } as unknown as Graph; 311 - 312 - const order: Array<string> = []; 313 - walkTopological(graph, (p) => order.push(p)); 314 - expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent)); 315 - }); 316 - 317 - it('preserves declaration order for equal-priority items (stability)', () => { 318 - const a = '#/components/schemas/A'; 319 - const b = '#/components/schemas/B'; 320 - const c = '#/components/schemas/C'; 321 - const nodes = [a, b, c]; 322 - const graph = makeGraph([], nodes); 323 - const order: Array<string> = []; 324 - walkTopological(graph, (p) => order.push(p)); 325 - expect(order).toEqual(nodes); 326 - }); 327 - });
+1 -1
packages/openapi-ts/src/ir/context.ts
··· 3 3 import type { Package } from '~/config/utils/package'; 4 4 import { packageFactory } from '~/config/utils/package'; 5 5 import { TypeScriptRenderer } from '~/generate/renderer'; 6 - import type { Graph } from '~/openApi/shared/utils/graph'; 6 + import type { Graph } from '~/graph'; 7 7 import { buildName } from '~/openApi/shared/utils/name'; 8 8 import type { PluginConfigMap } from '~/plugins/config'; 9 9 import { PluginInstance } from '~/plugins/shared/utils/instance';
+19 -194
packages/openapi-ts/src/ir/graph.ts
··· 1 - import type { Graph, NodeInfo } from '~/openApi/shared/utils/graph'; 2 - import { MinHeap } from '~/utils/minHeap'; 3 - 4 - type KindPriority = Record<IrTopLevelKind, number>; 5 - type PreferGroups = ReadonlyArray<IrTopLevelKind>; 6 - type PriorityFn = (pointer: string) => number; 7 - 8 - /** 9 - * Walks the nodes of the graph in topological order (dependencies before dependents). 10 - * Calls the callback for each node pointer in order. 11 - * Nodes in cycles are grouped together and emitted in arbitrary order within the group. 12 - * 13 - * @param graph - The dependency graph 14 - * @param callback - Function to call for each node pointer 15 - */ 16 - export const walkTopological = ( 17 - graph: Graph, 18 - callback: (pointer: string, nodeInfo: NodeInfo) => void, 19 - options?: { 20 - preferGroups?: PreferGroups; 21 - priority?: PriorityFn; 22 - }, 23 - ) => { 24 - // Stable Kahn's algorithm that respects declaration order as a tiebreaker. 25 - const pointers = Array.from(graph.nodes.keys()); 26 - // Base insertion order 27 - const baseIndex = new Map<string, number>(); 28 - pointers.forEach((pointer, index) => baseIndex.set(pointer, index)); 29 - 30 - // Composite decl index: group priority then base insertion order 31 - const declIndex = new Map<string, number>(); 32 - const priorityFn = options?.priority ?? defaultPriorityFn; 33 - for (const pointer of pointers) { 34 - const group = priorityFn(pointer) ?? 10; 35 - const composite = group * 1_000_000 + (baseIndex.get(pointer) ?? 0); 36 - declIndex.set(pointer, composite); 37 - } 38 - 39 - // Build dependency sets for each pointer (prefer subtreeDependencies, fall back to nodeDependencies) 40 - const depsOf = new Map<string, Set<string>>(); 41 - for (const pointer of pointers) { 42 - const raw = 43 - graph.subtreeDependencies?.get(pointer) ?? 44 - graph.nodeDependencies?.get(pointer) ?? 45 - new Set(); 46 - const filtered = new Set<string>(); 47 - for (const rawPointer of raw) { 48 - if (rawPointer === pointer) continue; // ignore self-dependencies for ordering 49 - if (graph.nodes.has(rawPointer)) { 50 - filtered.add(rawPointer); 51 - } 52 - } 53 - depsOf.set(pointer, filtered); 54 - } 55 - 56 - // Build inDegree and dependents adjacency 57 - const inDegree = new Map<string, number>(); 58 - const dependents = new Map<string, Set<string>>(); 59 - for (const pointer of pointers) { 60 - inDegree.set(pointer, 0); 61 - } 62 - for (const [pointer, deps] of depsOf) { 63 - inDegree.set(pointer, deps.size); 64 - for (const d of deps) { 65 - if (!dependents.has(d)) { 66 - dependents.set(d, new Set()); 67 - } 68 - dependents.get(d)!.add(pointer); 69 - } 70 - } 71 - 72 - // Helper to sort pointers by declaration order 73 - const sortByDecl = (arr: Array<string>) => 74 - arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!); 75 - 76 - // Initialize queue with zero-inDegree nodes in declaration order 77 - // Use a small binary min-heap prioritized by declaration index to avoid repeated full sorts. 78 - const heap = new MinHeap(declIndex); 79 - for (const pointer of pointers) { 80 - if ((inDegree.get(pointer) ?? 0) === 0) { 81 - heap.push(pointer); 82 - } 83 - } 84 - 85 - const emitted = new Set<string>(); 86 - const order: Array<string> = []; 87 - 88 - while (!heap.isEmpty()) { 89 - const cur = heap.pop()!; 90 - if (emitted.has(cur)) continue; 91 - emitted.add(cur); 92 - order.push(cur); 93 - 94 - const deps = dependents.get(cur); 95 - if (!deps) continue; 96 - 97 - for (const dep of deps) { 98 - const v = (inDegree.get(dep) ?? 0) - 1; 99 - inDegree.set(dep, v); 100 - if (v === 0) { 101 - heap.push(dep); 102 - } 103 - } 104 - } 105 - 106 - // emit remaining nodes (cycles) in declaration order 107 - const remaining = pointers.filter((pointer) => !emitted.has(pointer)); 108 - sortByDecl(remaining); 109 - for (const pointer of remaining) { 110 - emitted.add(pointer); 111 - order.push(pointer); 112 - } 113 - 114 - // prefer specified groups when safe 115 - let finalOrder = order; 116 - const preferGroups = options?.preferGroups ?? defaultPreferGroups; 117 - if (preferGroups && preferGroups.length > 0) { 118 - // build group priority map (lower = earlier) 119 - const groupPriority = new Map<string, number>(); 120 - for (let i = 0; i < preferGroups.length; i++) { 121 - const k = preferGroups[i]; 122 - if (k) { 123 - groupPriority.set(k, i); 124 - } 125 - } 126 - 127 - const getGroup: PriorityFn = (pointer) => { 128 - const result = matchIrTopLevelPointer(pointer); 129 - if (result.matched) { 130 - return groupPriority.has(result.kind) 131 - ? groupPriority.get(result.kind)! 132 - : preferGroups.length; 133 - } 134 - return preferGroups.length; 135 - }; 136 - 137 - // proposed order: sort by (groupPriority, originalIndex) 138 - const proposed = [...order].sort((a, b) => { 139 - const ga = getGroup(a); 140 - const gb = getGroup(b); 141 - return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b); 142 - }); 143 - 144 - // Build quick lookup of original index and proposed index 145 - const proposedIndex = new Map<string, number>(); 146 - for (let i = 0; i < proposed.length; i++) { 147 - proposedIndex.set(proposed[i]!, i); 148 - } 149 - 150 - // Micro-optimization: only validate edges where group(dep) > group(node) 151 - const violated = (() => { 152 - for (const [node, deps] of depsOf) { 153 - for (const dep of deps) { 154 - const gDep = getGroup(dep); 155 - const gNode = getGroup(node); 156 - if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping 157 - const pDep = proposedIndex.get(dep)!; 158 - const pNode = proposedIndex.get(node)!; 159 - if (pDep >= pNode) { 160 - return true; 161 - } 162 - } 163 - } 164 - return false; 165 - })(); 166 - 167 - if (!violated) { 168 - finalOrder = proposed; 169 - } 170 - } 171 - 172 - // Finally, call back in final order 173 - for (const pointer of finalOrder) { 174 - callback(pointer, graph.nodes.get(pointer)!); 175 - } 176 - }; 1 + import type { GetPointerPriorityFn, PointerGroupMatch } from '~/graph'; 177 2 178 3 export const irTopLevelKinds = [ 179 4 'operation', ··· 185 10 ] as const; 186 11 187 12 export type IrTopLevelKind = (typeof irTopLevelKinds)[number]; 188 - 189 - export type IrTopLevelPointerMatch = 190 - | { kind: IrTopLevelKind; matched: true } 191 - | { kind?: undefined; matched: false }; 192 13 193 14 /** 194 15 * Checks if a pointer matches a known top-level IR component (schema, parameter, etc) and returns match info. ··· 197 18 * @param kind - (Optional) The component kind to check 198 19 * @returns { matched: true, kind: IrTopLevelKind } | { matched: false } - Whether it matched, and the matched kind if so 199 20 */ 200 - export const matchIrTopLevelPointer = ( 21 + export const matchIrPointerToGroup = ( 201 22 pointer: string, 202 23 kind?: IrTopLevelKind, 203 - ): IrTopLevelPointerMatch => { 24 + ): PointerGroupMatch<IrTopLevelKind> => { 204 25 const patterns: Record<IrTopLevelKind, RegExp> = { 205 26 operation: 206 27 /^#\/paths\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/, ··· 226 47 }; 227 48 228 49 // default grouping preference (earlier groups emitted first when safe) 229 - export const defaultPreferGroups = [ 50 + export const preferGroups = [ 51 + 'server', 230 52 'schema', 231 53 'parameter', 232 54 'requestBody', 233 55 'operation', 234 - 'server', 235 56 'webhook', 236 - ] satisfies PreferGroups; 57 + ] satisfies ReadonlyArray<IrTopLevelKind>; 58 + 59 + type KindPriority = Record<IrTopLevelKind, number>; 237 60 238 61 // default group priority (lower = earlier) 239 - // built from `defaultPreferGroups` so the priority order stays in sync with the prefer-groups array. 240 - const defaultKindPriority: KindPriority = (() => { 62 + // built from `preferGroups` so the priority order stays in sync with the prefer-groups array. 63 + const kindPriority: KindPriority = (() => { 241 64 const partial: Partial<KindPriority> = {}; 242 - for (let i = 0; i < defaultPreferGroups.length; i++) { 243 - const k = defaultPreferGroups[i]; 65 + for (let i = 0; i < preferGroups.length; i++) { 66 + const k = preferGroups[i]; 244 67 if (k) partial[k] = i; 245 68 } 246 69 // Ensure all known kinds exist in the map (fall back to a high index). 247 70 for (const k of irTopLevelKinds) { 248 71 if (partial[k] === undefined) { 249 - partial[k] = defaultPreferGroups.length; 72 + partial[k] = preferGroups.length; 250 73 } 251 74 } 252 75 return partial as KindPriority; 253 76 })(); 254 77 255 - const defaultPriorityFn: PriorityFn = (pointer) => { 256 - const result = matchIrTopLevelPointer(pointer); 78 + const defaultPriority = 10; 79 + 80 + export const getIrPointerPriority: GetPointerPriorityFn = (pointer) => { 81 + const result = matchIrPointerToGroup(pointer); 257 82 if (result.matched) { 258 - return defaultKindPriority[result.kind] ?? 10; 83 + return kindPriority[result.kind] ?? defaultPriority; 259 84 } 260 - return 10; 85 + return defaultPriority; 261 86 };
+1 -1
packages/openapi-ts/src/openApi/shared/graph/meta.ts
··· 1 + import type { Graph } from '~/graph'; 1 2 import { createOperationKey } from '~/ir/operation'; 2 3 import type { Logger } from '~/utils/logger'; 3 4 import { jsonPointerToPath } from '~/utils/ref'; 4 5 5 6 import { addNamespace, stringToNamespace } from '../utils/filter'; 6 - import type { Graph } from '../utils/graph'; 7 7 import { httpMethods } from '../utils/operation'; 8 8 9 9 export type ResourceMetadata = {
+2 -1
packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts
··· 1 + import type { Graph } from '~/graph'; 1 2 import type { Logger } from '~/utils/logger'; 2 3 import { jsonPointerToPath } from '~/utils/ref'; 3 4 4 5 import type { Config } from '../../../types/config'; 5 6 import deepEqual from '../utils/deepEqual'; 6 - import { buildGraph, type Graph, type Scope } from '../utils/graph'; 7 + import { buildGraph, type Scope } from '../utils/graph'; 7 8 import { buildName } from '../utils/name'; 8 9 import { deepClone } from '../utils/schema'; 9 10 import { childSchemaRelationships } from '../utils/schemaChildRelationships';
+1 -61
packages/openapi-ts/src/openApi/shared/utils/graph.ts
··· 1 + import type { Graph, NodeInfo } from '~/graph'; 1 2 import type { Logger } from '~/utils/logger'; 2 3 import { normalizeJsonPointer, pathToJsonPointer } from '~/utils/ref'; 3 4 ··· 10 11 * - 'write': Node is write-only (e.g., writeOnly: true). 11 12 */ 12 13 export type Scope = 'normal' | 'read' | 'write'; 13 - 14 - /** 15 - * Information about a node in the OpenAPI graph. 16 - * 17 - * @property deprecated - Whether the node is deprecated. Optional. 18 - * @property key - The property name or array index in the parent, or null for root. 19 - * @property node - The actual object at this pointer in the spec. 20 - * @property parentPointer - The JSON Pointer of the parent node, or null for root. 21 - * @property scopes - The set of access scopes for this node, if any. Optional. 22 - * @property tags - The set of tags for this node, if any. Optional. 23 - */ 24 - export type NodeInfo = { 25 - /** Whether the node is deprecated. Optional. */ 26 - deprecated?: boolean; 27 - /** The property name or array index in the parent, or null for root. */ 28 - key: string | number | null; 29 - /** The actual object at this pointer in the spec. */ 30 - node: unknown; 31 - /** The JSON Pointer of the parent node, or null for root. */ 32 - parentPointer: string | null; 33 - /** The set of access scopes for this node, if any. Optional. */ 34 - scopes?: Set<Scope>; 35 - /** The set of tags for this node, if any. Optional. */ 36 - tags?: Set<string>; 37 - }; 38 - 39 - /** 40 - * The main graph structure for OpenAPI node analysis. 41 - * 42 - * @property nodeDependencies - For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. Nodes with no dependencies are omitted. 43 - * @property nodes - Map from normalized JSON Pointer to NodeInfo for every node in the spec. 44 - * @property reverseNodeDependencies - For each node with at least one dependent, the set of nodes that reference it via $ref. Nodes with no dependents are omitted. 45 - */ 46 - export type Graph = { 47 - /** 48 - * For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. 49 - * Nodes with no dependencies are omitted from this map. 50 - */ 51 - nodeDependencies: Map<string, Set<string>>; 52 - /** 53 - * Map from normalized JSON Pointer to NodeInfo for every node in the spec. 54 - */ 55 - nodes: Map<string, NodeInfo>; 56 - /** 57 - * For each node with at least one dependent, the set of nodes that reference it via $ref. 58 - * Nodes with no dependents are omitted from this map. 59 - */ 60 - reverseNodeDependencies: Map<string, Set<string>>; 61 - /** 62 - * For each node, the set of direct $ref targets that appear anywhere inside the node's 63 - * subtree (the node itself and its children). This is populated during graph construction 64 - * and is used to compute top-level dependency relationships where $ref may be attached to 65 - * child pointers instead of the parent. 66 - */ 67 - subtreeDependencies: Map<string, Set<string>>; 68 - /** 69 - * For each node, the set of all (transitive) normalized JSON Pointers it references via $ref anywhere in its subtree. 70 - * This includes both direct and indirect dependencies, making it useful for filtering, codegen, and tree-shaking. 71 - */ 72 - transitiveDependencies: Map<string, Set<string>>; 73 - }; 74 14 75 15 /** 76 16 * Ensures every relevant child node (e.g., properties, items) in the graph has a `scopes` property.
-19
packages/openapi-ts/src/plugins/shared/types/instance.d.ts
··· 47 47 WalkEvents, 48 48 { type: T } 49 49 >; 50 - 51 - export type WalkOptions = { 52 - /** 53 - * Order of walking schemas. 54 - * 55 - * The "declarations" option ensures that schemas are walked in the order 56 - * they are declared in the input document. This is useful for scenarios where 57 - * the order of declaration matters, such as when generating code that relies 58 - * on the sequence of schema definitions. 59 - * 60 - * The "topological" option ensures that schemas are walked in an order 61 - * where dependencies are visited before the schemas that depend on them. 62 - * This is useful for scenarios where you need to process or generate 63 - * schemas in a way that respects their interdependencies. 64 - * 65 - * @default 'topological' 66 - */ 67 - order?: 'declarations' | 'topological'; 68 - };
+23 -121
packages/openapi-ts/src/plugins/shared/utils/instance.ts
··· 8 8 } from '@hey-api/codegen-core'; 9 9 10 10 import { HeyApiError } from '~/error'; 11 + import type { WalkOptions } from '~/graph'; 12 + import { walk } from '~/graph'; 11 13 import type { IrTopLevelKind } from '~/ir/graph'; 12 14 import { 15 + getIrPointerPriority, 13 16 irTopLevelKinds, 14 - matchIrTopLevelPointer, 15 - walkTopological, 17 + matchIrPointerToGroup, 18 + preferGroups, 16 19 } from '~/ir/graph'; 17 20 import type { IR } from '~/ir/types'; 18 21 import type { OpenApi } from '~/openApi/types'; ··· 21 24 import type { Plugin } from '~/plugins/types'; 22 25 import { jsonPointerToPath } from '~/utils/ref'; 23 26 24 - import type { WalkEvent, WalkOptions } from '../types/instance'; 27 + import type { WalkEvent } from '../types/instance'; 25 28 26 29 const defaultGetFilePath = (symbol: Symbol): string | undefined => { 27 30 if (!symbol.meta?.pluginName || typeof symbol.meta.pluginName !== 'string') { ··· 145 148 options: any, 146 149 ] 147 150 ): void { 151 + if (!this.context.graph) { 152 + throw new Error('No graph available in context'); 153 + } 154 + 148 155 let callback: (event: WalkEvent<T>) => void; 149 156 let events: ReadonlyArray<T>; 150 - let options: Required<WalkOptions> = { 157 + let options: WalkOptions = { 151 158 order: 'topological', 152 159 }; 153 160 if (typeof args[args.length - 1] === 'function') { ··· 163 170 } 164 171 const eventSet = new Set(events.length ? events : irTopLevelKinds); 165 172 166 - if (options.order === 'declarations') { 167 - if (eventSet.has('server') && this.context.ir.servers) { 168 - for (const server of this.context.ir.servers) { 169 - const event: WalkEvent<'server'> = { 170 - _path: ['servers', String(this.context.ir.servers.indexOf(server))], 171 - server, 172 - type: 'server', 173 - }; 174 - try { 175 - callback(event as WalkEvent<T>); 176 - } catch (error) { 177 - this.forEachError(error, event); 178 - } 179 - } 180 - } 181 - 182 - if (eventSet.has('schema') && this.context.ir.components?.schemas) { 183 - for (const name in this.context.ir.components.schemas) { 184 - const event: WalkEvent<'schema'> = { 185 - $ref: `#/components/schemas/${name}`, 186 - _path: ['components', 'schemas', name], 187 - name, 188 - schema: this.context.ir.components.schemas[name]!, 189 - type: 'schema', 190 - }; 191 - try { 192 - callback(event as WalkEvent<T>); 193 - } catch (error) { 194 - this.forEachError(error, event); 195 - } 196 - } 197 - } 198 - 199 - if (eventSet.has('parameter') && this.context.ir.components?.parameters) { 200 - for (const name in this.context.ir.components.parameters) { 201 - const event: WalkEvent<'parameter'> = { 202 - $ref: `#/components/parameters/${name}`, 203 - _path: ['components', 'parameters', name], 204 - name, 205 - parameter: this.context.ir.components.parameters[name]!, 206 - type: 'parameter', 207 - }; 208 - try { 209 - callback(event as WalkEvent<T>); 210 - } catch (error) { 211 - this.forEachError(error, event); 212 - } 213 - } 214 - } 215 - 216 - if ( 217 - eventSet.has('requestBody') && 218 - this.context.ir.components?.requestBodies 219 - ) { 220 - for (const name in this.context.ir.components.requestBodies) { 221 - const event: WalkEvent<'requestBody'> = { 222 - $ref: `#/components/requestBodies/${name}`, 223 - _path: ['components', 'requestBodies', name], 224 - name, 225 - requestBody: this.context.ir.components.requestBodies[name]!, 226 - type: 'requestBody', 227 - }; 228 - try { 229 - callback(event as WalkEvent<T>); 230 - } catch (error) { 231 - this.forEachError(error, event); 232 - } 233 - } 234 - } 235 - 236 - if (eventSet.has('operation') && this.context.ir.paths) { 237 - for (const path in this.context.ir.paths) { 238 - const pathItem = 239 - this.context.ir.paths[path as keyof typeof this.context.ir.paths]; 240 - for (const _method in pathItem) { 241 - const method = _method as keyof typeof pathItem; 242 - const event: WalkEvent<'operation'> = { 243 - _path: ['paths', path, method], 244 - method, 245 - operation: pathItem[method]!, 246 - path, 247 - type: 'operation', 248 - }; 249 - try { 250 - callback(event as WalkEvent<T>); 251 - } catch (error) { 252 - this.forEachError(error, event); 253 - } 254 - } 255 - } 256 - } 257 - 258 - if (eventSet.has('webhook') && this.context.ir.webhooks) { 259 - for (const key in this.context.ir.webhooks) { 260 - const webhook = this.context.ir.webhooks[key]; 261 - for (const _method in webhook) { 262 - const method = _method as keyof typeof webhook; 263 - const event: WalkEvent<'webhook'> = { 264 - _path: ['webhooks', key, method], 265 - key, 266 - method, 267 - operation: webhook[method]!, 268 - type: 'webhook', 269 - }; 270 - try { 271 - callback(event as WalkEvent<T>); 272 - } catch (error) { 273 - this.forEachError(error, event); 274 - } 275 - } 276 - } 277 - } 278 - } else if (options.order === 'topological' && this.context.graph) { 279 - walkTopological(this.context.graph, (pointer, nodeInfo) => { 280 - const result = matchIrTopLevelPointer(pointer); 173 + walk( 174 + this.context.graph, 175 + (pointer, nodeInfo) => { 176 + const result = matchIrPointerToGroup(pointer); 281 177 if (!result.matched || !eventSet.has(result.kind)) return; 282 178 let event: WalkEvent | undefined; 283 179 switch (result.kind) { ··· 341 237 this.forEachError(error, event); 342 238 } 343 239 } 344 - }); 345 - } 240 + }, 241 + { 242 + getPointerPriority: getIrPointerPriority, 243 + matchPointerToGroup: matchIrPointerToGroup, 244 + order: options.order, 245 + preferGroups, 246 + }, 247 + ); 346 248 } 347 249 348 250 /**