···11+import { describe, expect, it } from 'vitest';
22+33+import type { Graph } from '~/graph';
44+import {
55+ getIrPointerPriority,
66+ matchIrPointerToGroup,
77+ preferGroups,
88+} from '~/ir/graph';
99+import { buildGraph } from '~/openApi/shared/utils/graph';
1010+1111+import { walk } from '../walk';
1212+1313+const loggerStub = {
1414+ timeEvent: () => ({ timeEnd: () => {} }),
1515+} as any;
1616+1717+describe('walkTopological', () => {
1818+ const makeGraph = (
1919+ deps: Array<[string, Array<string>]>,
2020+ nodes: Array<string>,
2121+ ) => {
2222+ const nodeDependencies = new Map<string, Set<string>>();
2323+ const subtreeDependencies = new Map<string, Set<string>>();
2424+ const reverseNodeDependencies = new Map<string, Set<string>>();
2525+ const nodesMap = new Map<string, any>();
2626+2727+ for (const name of nodes) {
2828+ nodesMap.set(name, { key: null, node: {}, parentPointer: null });
2929+ }
3030+3131+ for (const [from, toList] of deps) {
3232+ const s = new Set<string>(toList);
3333+ nodeDependencies.set(from, s);
3434+ subtreeDependencies.set(from, new Set<string>(toList));
3535+ for (const to of toList) {
3636+ if (!reverseNodeDependencies.has(to))
3737+ reverseNodeDependencies.set(to, new Set());
3838+ reverseNodeDependencies.get(to)!.add(from);
3939+ }
4040+ }
4141+4242+ return {
4343+ nodeDependencies,
4444+ nodes: nodesMap,
4545+ reverseNodeDependencies,
4646+ subtreeDependencies,
4747+ transitiveDependencies: new Map<string, Set<string>>(),
4848+ } as unknown as Graph;
4949+ };
5050+5151+ it('walks nodes in topological order for a simple acyclic graph', () => {
5252+ // Graph: A -> B -> C
5353+ const graph = makeGraph(
5454+ [
5555+ ['A', ['B']],
5656+ ['B', ['C']],
5757+ ],
5858+ ['A', 'B', 'C'],
5959+ );
6060+ const order: Array<string> = [];
6161+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
6262+ expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
6363+ expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
6464+ expect(order).toHaveLength(3);
6565+ });
6666+6767+ it('walks nodes in topological order for multiple roots', () => {
6868+ // Graph: A -> B, C -> D
6969+ const graph = makeGraph(
7070+ [
7171+ ['A', ['B']],
7272+ ['C', ['D']],
7373+ ],
7474+ ['A', 'B', 'C', 'D'],
7575+ );
7676+ const order: Array<string> = [];
7777+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
7878+ expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
7979+ expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
8080+ expect(order).toHaveLength(4);
8181+ });
8282+8383+ it('walks nodes in topological order for a disconnected graph', () => {
8484+ // Graph: A -> B, C (no deps), D (no deps)
8585+ const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']);
8686+ const order: Array<string> = [];
8787+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
8888+ expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
8989+ expect(order).toHaveLength(4);
9090+ expect(order).toContain('C');
9191+ expect(order).toContain('D');
9292+ });
9393+9494+ it('walks nodes in topological order for a diamond dependency', () => {
9595+ // Graph: A
9696+ // / \
9797+ // B C
9898+ // \ /
9999+ // D
100100+ const graph = makeGraph(
101101+ [
102102+ ['A', ['B', 'C']],
103103+ ['B', ['D']],
104104+ ['C', ['D']],
105105+ ],
106106+ ['A', 'B', 'C', 'D'],
107107+ );
108108+ const order: Array<string> = [];
109109+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
110110+ expect(order.indexOf('D')).toBeLessThan(order.indexOf('B'));
111111+ expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
112112+ expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
113113+ expect(order.indexOf('C')).toBeLessThan(order.indexOf('A'));
114114+ expect(order).toHaveLength(4);
115115+ });
116116+117117+ it('walks nodes in topological order for a long chain', () => {
118118+ // Graph: A -> B -> C -> D -> E
119119+ const graph = makeGraph(
120120+ [
121121+ ['A', ['B']],
122122+ ['B', ['C']],
123123+ ['C', ['D']],
124124+ ['D', ['E']],
125125+ ],
126126+ ['A', 'B', 'C', 'D', 'E'],
127127+ );
128128+ const order: Array<string> = [];
129129+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
130130+ expect(order.indexOf('E')).toBeLessThan(order.indexOf('D'));
131131+ expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
132132+ expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
133133+ expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
134134+ expect(order).toHaveLength(5);
135135+ });
136136+137137+ it('walks all nodes, including cycles', () => {
138138+ // Graph: A <-> B (cycle), C (no deps)
139139+ const graph = makeGraph(
140140+ [
141141+ ['A', ['B']],
142142+ ['B', ['A']],
143143+ ],
144144+ ['A', 'B', 'C'],
145145+ );
146146+ const order: Array<string> = [];
147147+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
148148+ expect(order.sort()).toEqual(['A', 'B', 'C']);
149149+ });
150150+151151+ it('matches ordering for validators-circular-ref spec', async () => {
152152+ const specModule = await import(
153153+ '../../../../../specs/3.1.x/validators-circular-ref.json'
154154+ );
155155+ const spec = specModule.default ?? specModule;
156156+ const { graph } = buildGraph(spec, loggerStub);
157157+158158+ const order: Array<string> = [];
159159+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
160160+161161+ const foo = '#/components/schemas/Foo';
162162+ const bar = '#/components/schemas/Bar';
163163+ const baz = '#/components/schemas/Baz';
164164+ const qux = '#/components/schemas/Qux';
165165+166166+ // Bar should come before Foo because Foo depends on Bar
167167+ expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo));
168168+169169+ // Baz and Qux form a mutual $ref cycle; both must be present
170170+ expect(order).toContain(baz);
171171+ expect(order).toContain(qux);
172172+ });
173173+174174+ it('prefers schema group before parameter when safe (default)', () => {
175175+ // parameter then schema in declaration order, no deps -> schema should move before parameter
176176+ const param = '#/components/parameters/P';
177177+ const schema = '#/components/schemas/A';
178178+ const nodes = [param, schema];
179179+ const graph = makeGraph([], nodes);
180180+181181+ const order: Array<string> = [];
182182+ walk(graph, (pointer) => order.push(pointer), {
183183+ getPointerPriority: getIrPointerPriority,
184184+ matchPointerToGroup: matchIrPointerToGroup,
185185+ order: 'topological',
186186+ preferGroups,
187187+ });
188188+ expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param));
189189+ });
190190+191191+ it('does not apply preferGroups when it would violate dependencies (fallback)', () => {
192192+ // declaration order: param, schema; schema depends on param -> cannot move before param
193193+ const param = '#/components/parameters/P';
194194+ const schema = '#/components/schemas/S';
195195+ const nodes = [param, schema];
196196+ const nodeDependencies = new Map<string, Set<string>>();
197197+ nodeDependencies.set(schema, new Set([param]));
198198+ const subtreeDependencies = new Map<string, Set<string>>();
199199+ const reverseNodeDependencies = new Map<string, Set<string>>();
200200+ const nodesMap = new Map<string, any>();
201201+ for (const n of nodes)
202202+ nodesMap.set(n, { key: null, node: {}, parentPointer: null });
203203+ const graph = {
204204+ nodeDependencies,
205205+ nodes: nodesMap,
206206+ reverseNodeDependencies,
207207+ subtreeDependencies,
208208+ transitiveDependencies: new Map<string, Set<string>>(),
209209+ } as unknown as Graph;
210210+211211+ const order: Array<string> = [];
212212+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
213213+ // schema depends on param so param must remain before schema
214214+ expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema));
215215+ });
216216+217217+ it('ignores self-dependencies when ordering', () => {
218218+ // Foo has self-ref only, Bar references Foo -> Foo should come before Bar
219219+ const foo = '#/components/schemas/Foo';
220220+ const bar = '#/components/schemas/Bar';
221221+ const nodes = [foo, bar];
222222+ const nodeDependencies = new Map<string, Set<string>>();
223223+ nodeDependencies.set(foo, new Set([foo]));
224224+ nodeDependencies.set(bar, new Set([foo]));
225225+226226+ const nodesMap = new Map<string, any>();
227227+ for (const n of nodes)
228228+ nodesMap.set(n, { key: null, node: {}, parentPointer: null });
229229+230230+ const graph = {
231231+ nodeDependencies,
232232+ nodes: nodesMap,
233233+ reverseNodeDependencies: new Map<string, Set<string>>(),
234234+ subtreeDependencies: new Map<string, Set<string>>(),
235235+ transitiveDependencies: new Map<string, Set<string>>(),
236236+ } as unknown as Graph;
237237+238238+ const order: Array<string> = [];
239239+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
240240+ // Foo is a dependency of Bar, so Foo should come before Bar
241241+ expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar));
242242+ });
243243+244244+ it('uses subtreeDependencies when nodeDependencies are absent', () => {
245245+ const parent = '#/components/schemas/Parent';
246246+ const child = '#/components/schemas/Child';
247247+ const nodes = [parent, child];
248248+ const nodeDependencies = new Map<string, Set<string>>();
249249+ const subtreeDependencies = new Map<string, Set<string>>();
250250+ subtreeDependencies.set(parent, new Set([child]));
251251+252252+ const nodesMap = new Map<string, any>();
253253+ for (const n of nodes)
254254+ nodesMap.set(n, { key: null, node: {}, parentPointer: null });
255255+256256+ const graph = {
257257+ nodeDependencies,
258258+ nodes: nodesMap,
259259+ reverseNodeDependencies: new Map<string, Set<string>>(),
260260+ subtreeDependencies,
261261+ transitiveDependencies: new Map<string, Set<string>>(),
262262+ } as unknown as Graph;
263263+264264+ const order: Array<string> = [];
265265+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
266266+ expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent));
267267+ });
268268+269269+ it('preserves declaration order for equal-priority items (stability)', () => {
270270+ const a = '#/components/schemas/A';
271271+ const b = '#/components/schemas/B';
272272+ const c = '#/components/schemas/C';
273273+ const nodes = [a, b, c];
274274+ const graph = makeGraph([], nodes);
275275+ const order: Array<string> = [];
276276+ walk(graph, (pointer) => order.push(pointer), { order: 'topological' });
277277+ expect(order).toEqual(nodes);
278278+ });
279279+280280+ it('walks nodes in declaration order when order=declarations', () => {
281281+ const a = '#/components/schemas/A';
282282+ const b = '#/components/schemas/B';
283283+ const c = '#/components/schemas/C';
284284+ const nodes = [a, b, c];
285285+ const graph = makeGraph([], nodes);
286286+ const order: Array<string> = [];
287287+ walk(graph, (pointer) => order.push(pointer), { order: 'declarations' });
288288+ expect(order).toEqual(nodes);
289289+ });
290290+291291+ it('applies preferGroups ordering in declaration mode', () => {
292292+ const param = '#/components/parameters/P';
293293+ const schema = '#/components/schemas/A';
294294+ const nodes = [param, schema];
295295+ const graph = makeGraph([], nodes);
296296+297297+ const order: Array<string> = [];
298298+ walk(graph, (pointer) => order.push(pointer), {
299299+ matchPointerToGroup: matchIrPointerToGroup,
300300+ order: 'declarations',
301301+ preferGroups,
302302+ });
303303+ // preferGroups puts schema before parameter
304304+ expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param));
305305+ });
306306+});
+7
packages/openapi-ts/src/graph/index.ts
···11+export type { Graph, NodeInfo } from './types/graph';
22+export type {
33+ GetPointerPriorityFn,
44+ PointerGroupMatch,
55+ WalkOptions,
66+} from './types/walk';
77+export { walk } from './walk';
+60
packages/openapi-ts/src/graph/types/graph.d.ts
···11+/**
22+ * The main graph structure for OpenAPI node analysis.
33+ *
44+ * @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.
55+ * @property nodes - Map from normalized JSON Pointer to NodeInfo for every node in the spec.
66+ * @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.
77+ */
88+export type Graph = {
99+ /**
1010+ * For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref.
1111+ * Nodes with no dependencies are omitted from this map.
1212+ */
1313+ nodeDependencies: Map<string, Set<string>>;
1414+ /**
1515+ * Map from normalized JSON Pointer to NodeInfo for every node in the spec.
1616+ */
1717+ nodes: Map<string, NodeInfo>;
1818+ /**
1919+ * For each node with at least one dependent, the set of nodes that reference it via $ref.
2020+ * Nodes with no dependents are omitted from this map.
2121+ */
2222+ reverseNodeDependencies: Map<string, Set<string>>;
2323+ /**
2424+ * For each node, the set of direct $ref targets that appear anywhere inside the node's
2525+ * subtree (the node itself and its children). This is populated during graph construction
2626+ * and is used to compute top-level dependency relationships where $ref may be attached to
2727+ * child pointers instead of the parent.
2828+ */
2929+ subtreeDependencies: Map<string, Set<string>>;
3030+ /**
3131+ * For each node, the set of all (transitive) normalized JSON Pointers it references via $ref anywhere in its subtree.
3232+ * This includes both direct and indirect dependencies, making it useful for filtering, codegen, and tree-shaking.
3333+ */
3434+ transitiveDependencies: Map<string, Set<string>>;
3535+};
3636+3737+/**
3838+ * Information about a node in the OpenAPI graph.
3939+ *
4040+ * @property deprecated - Whether the node is deprecated. Optional.
4141+ * @property key - The property name or array index in the parent, or null for root.
4242+ * @property node - The actual object at this pointer in the spec.
4343+ * @property parentPointer - The JSON Pointer of the parent node, or null for root.
4444+ * @property scopes - The set of access scopes for this node, if any. Optional.
4545+ * @property tags - The set of tags for this node, if any. Optional.
4646+ */
4747+export type NodeInfo = {
4848+ /** Whether the node is deprecated. Optional. */
4949+ deprecated?: boolean;
5050+ /** The property name or array index in the parent, or null for root. */
5151+ key: string | number | null;
5252+ /** The actual object at this pointer in the spec. */
5353+ node: unknown;
5454+ /** The JSON Pointer of the parent node, or null for root. */
5555+ parentPointer: string | null;
5656+ /** The set of access scopes for this node, if any. Optional. */
5757+ scopes?: Set<Scope>;
5858+ /** The set of tags for this node, if any. Optional. */
5959+ tags?: Set<string>;
6060+};
+54
packages/openapi-ts/src/graph/types/walk.d.ts
···11+import type { Graph, NodeInfo } from './graph';
22+33+export type WalkCallbackFn = (pointer: string, nodeInfo: NodeInfo) => void;
44+55+export type GetPointerPriorityFn = (pointer: string) => number;
66+77+export type PointerGroupMatch<T extends string = string> =
88+ | { kind: T; matched: true }
99+ | { kind?: undefined; matched: false };
1010+1111+export type WalkOptions<T extends string = string> = {
1212+ /**
1313+ * Optional priority function used to compute a numeric priority for each
1414+ * pointer. Lower values are emitted earlier. Useful to customize ordering
1515+ * beyond the built-in group preferences.
1616+ */
1717+ getPointerPriority?: GetPointerPriorityFn;
1818+ /**
1919+ * Optional function to match a pointer to a group name.
2020+ *
2121+ * @param pointer The pointer string
2222+ * @returns The group name, or undefined if no match
2323+ */
2424+ matchPointerToGroup?: (pointer: string) => PointerGroupMatch<T>;
2525+ /**
2626+ * Order of walking schemas.
2727+ *
2828+ * The "declarations" option ensures that schemas are walked in the order
2929+ * they are declared in the input document. This is useful for scenarios where
3030+ * the order of declaration matters, such as when generating code that relies
3131+ * on the sequence of schema definitions.
3232+ *
3333+ * The "topological" option ensures that schemas are walked in an order
3434+ * where dependencies are visited before the schemas that depend on them.
3535+ * This is useful for scenarios where you need to process or generate
3636+ * schemas in a way that respects their interdependencies.
3737+ *
3838+ * @default 'topological'
3939+ */
4040+ order?: 'declarations' | 'topological';
4141+ /**
4242+ * Optional grouping preference for walking. When provided, walk function
4343+ * will prefer emitting kinds listed earlier in this array when it is safe
4444+ * to do so (it will only apply the preference when doing so does not
4545+ * violate dependency ordering).
4646+ */
4747+ preferGroups?: ReadonlyArray<T>;
4848+};
4949+5050+export type WalkFn = (
5151+ graph: Graph,
5252+ callback: WalkCallbackFn,
5353+ options?: WalkOptions,
5454+) => void;
+208
packages/openapi-ts/src/graph/walk.ts
···11+import { MinHeap } from '~/utils/minHeap';
22+33+import type { GetPointerPriorityFn, WalkFn } from './types/walk';
44+55+/**
66+ * Walk the nodes of the graph in declaration (insertion) order.
77+ * This is a cheap alternative to `walkTopological` when dependency ordering
88+ * is not required and the caller only wants nodes in the order they were
99+ * added to the graph.
1010+ */
1111+const walkDeclarations: WalkFn = (graph, callback, options) => {
1212+ const pointers = Array.from(graph.nodes.keys());
1313+1414+ if (options?.preferGroups && options.preferGroups.length > 0) {
1515+ // emit nodes that match each preferred group in order, preserving insertion order
1616+ const emitted = new Set<string>();
1717+ if (options.matchPointerToGroup) {
1818+ for (const kind of options.preferGroups) {
1919+ for (const pointer of pointers) {
2020+ const result = options.matchPointerToGroup(pointer);
2121+ if (!result.matched) continue;
2222+ if (result.kind === kind) {
2323+ emitted.add(pointer);
2424+ callback(pointer, graph.nodes.get(pointer)!);
2525+ }
2626+ }
2727+ }
2828+ }
2929+3030+ // emit anything not covered by the preferGroups (in declaration order)
3131+ for (const pointer of pointers) {
3232+ if (emitted.has(pointer)) continue;
3333+ callback(pointer, graph.nodes.get(pointer)!);
3434+ }
3535+ return;
3636+ }
3737+3838+ // fallback: simple declaration order
3939+ for (const pointer of pointers) {
4040+ callback(pointer, graph.nodes.get(pointer)!);
4141+ }
4242+};
4343+4444+/**
4545+ * Walks the nodes of the graph in topological order (dependencies before dependents).
4646+ * Calls the callback for each node pointer in order.
4747+ * Nodes in cycles are grouped together and emitted in arbitrary order within the group.
4848+ *
4949+ * @param graph - The dependency graph
5050+ * @param callback - Function to call for each node pointer
5151+ */
5252+const walkTopological: WalkFn = (graph, callback, options) => {
5353+ // stable Kahn's algorithm that respects declaration order as a tiebreaker.
5454+ const pointers = Array.from(graph.nodes.keys());
5555+ // base insertion order
5656+ const baseIndex = new Map<string, number>();
5757+ pointers.forEach((pointer, index) => baseIndex.set(pointer, index));
5858+5959+ // composite decl index: group priority then base insertion order
6060+ const declIndex = new Map<string, number>();
6161+ for (const pointer of pointers) {
6262+ const priority = options?.getPointerPriority?.(pointer) ?? 10;
6363+ const composite = priority * 1_000_000 + (baseIndex.get(pointer) ?? 0);
6464+ declIndex.set(pointer, composite);
6565+ }
6666+6767+ // build dependency sets for each pointer
6868+ const depsOf = new Map<string, Set<string>>();
6969+ for (const pointer of pointers) {
7070+ const raw = graph.subtreeDependencies?.get(pointer) ?? new Set();
7171+ const filtered = new Set<string>();
7272+ for (const rawPointer of raw) {
7373+ if (rawPointer === pointer) continue; // ignore self-dependencies for ordering
7474+ if (graph.nodes.has(rawPointer)) {
7575+ filtered.add(rawPointer);
7676+ }
7777+ }
7878+ depsOf.set(pointer, filtered);
7979+ }
8080+8181+ // build inDegree and dependents adjacency
8282+ const inDegree = new Map<string, number>();
8383+ const dependents = new Map<string, Set<string>>();
8484+ for (const pointer of pointers) {
8585+ inDegree.set(pointer, 0);
8686+ }
8787+ for (const [pointer, deps] of depsOf) {
8888+ inDegree.set(pointer, deps.size);
8989+ for (const d of deps) {
9090+ if (!dependents.has(d)) {
9191+ dependents.set(d, new Set());
9292+ }
9393+ dependents.get(d)!.add(pointer);
9494+ }
9595+ }
9696+9797+ // sort pointers by declaration order
9898+ const sortByDecl = (arr: Array<string>) =>
9999+ arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!);
100100+101101+ // initialize queue with zero-inDegree nodes in declaration order
102102+ // use min-heap prioritized by declaration index to avoid repeated full sorts
103103+ const heap = new MinHeap(declIndex);
104104+ for (const pointer of pointers) {
105105+ if ((inDegree.get(pointer) ?? 0) === 0) {
106106+ heap.push(pointer);
107107+ }
108108+ }
109109+110110+ const emitted = new Set<string>();
111111+ const order: Array<string> = [];
112112+113113+ while (!heap.isEmpty()) {
114114+ const cur = heap.pop()!;
115115+ if (emitted.has(cur)) continue;
116116+ emitted.add(cur);
117117+ order.push(cur);
118118+119119+ const deps = dependents.get(cur);
120120+ if (!deps) continue;
121121+122122+ for (const dep of deps) {
123123+ const v = (inDegree.get(dep) ?? 0) - 1;
124124+ inDegree.set(dep, v);
125125+ if (v === 0) {
126126+ heap.push(dep);
127127+ }
128128+ }
129129+ }
130130+131131+ // emit remaining nodes (cycles) in declaration order
132132+ const remaining = pointers.filter((pointer) => !emitted.has(pointer));
133133+ sortByDecl(remaining);
134134+ for (const pointer of remaining) {
135135+ emitted.add(pointer);
136136+ order.push(pointer);
137137+ }
138138+139139+ // prefer specified groups when safe
140140+ let finalOrder = order;
141141+ if (options?.preferGroups && options.preferGroups.length > 0) {
142142+ // build group priority map (lower = earlier)
143143+ const groupPriority = new Map<string, number>();
144144+ for (let i = 0; i < options.preferGroups.length; i++) {
145145+ const k = options.preferGroups[i];
146146+ if (k) {
147147+ groupPriority.set(k, i);
148148+ }
149149+ }
150150+151151+ const getGroup: GetPointerPriorityFn = (pointer) => {
152152+ if (options.matchPointerToGroup) {
153153+ const result = options.matchPointerToGroup(pointer);
154154+ if (result.matched) {
155155+ return groupPriority.has(result.kind)
156156+ ? groupPriority.get(result.kind)!
157157+ : options.preferGroups!.length;
158158+ }
159159+ }
160160+ return options.preferGroups!.length;
161161+ };
162162+163163+ // proposed order: sort by (groupPriority, originalIndex)
164164+ const proposed = [...order].sort((a, b) => {
165165+ const ga = getGroup(a);
166166+ const gb = getGroup(b);
167167+ return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b);
168168+ });
169169+170170+ // build quick lookup of original index and proposed index
171171+ const proposedIndex = new Map<string, number>();
172172+ for (let i = 0; i < proposed.length; i++) {
173173+ proposedIndex.set(proposed[i]!, i);
174174+ }
175175+176176+ // only validate edges where group(dep) > group(node)
177177+ const violated = (() => {
178178+ for (const [node, deps] of depsOf) {
179179+ for (const dep of deps) {
180180+ const gDep = getGroup(dep);
181181+ const gNode = getGroup(node);
182182+ if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping
183183+ const pDep = proposedIndex.get(dep)!;
184184+ const pNode = proposedIndex.get(node)!;
185185+ if (pDep >= pNode) {
186186+ return true;
187187+ }
188188+ }
189189+ }
190190+ return false;
191191+ })();
192192+193193+ if (!violated) {
194194+ finalOrder = proposed;
195195+ }
196196+ }
197197+198198+ for (const pointer of finalOrder) {
199199+ callback(pointer, graph.nodes.get(pointer)!);
200200+ }
201201+};
202202+203203+export const walk: WalkFn = (graph, callback, options) => {
204204+ if (options?.order === 'topological') {
205205+ return walkTopological(graph, callback, options);
206206+ }
207207+ return walkDeclarations(graph, callback, options);
208208+};
···11import { describe, expect, it } from 'vitest';
2233-import type { Graph } from '~/openApi/shared/utils/graph';
44-import { buildGraph } from '~/openApi/shared/utils/graph';
55-63import type { IrTopLevelKind } from '../graph';
77-import { matchIrTopLevelPointer, walkTopological } from '../graph';
88-99-// simple logger stub for buildGraph
1010-const loggerStub = {
1111- timeEvent: () => ({ timeEnd: () => {} }),
1212-} as any;
44+import { matchIrPointerToGroup } from '../graph';
1351414-describe('matchIrTopLevelPointer', () => {
66+describe('matchIrPointerToGroup', () => {
157 const cases: Array<
168 [
179 string,
···56485749 for (const [pointer, kind, expected] of cases) {
5850 it(`matches ${pointer} with kind=${kind}`, () => {
5959- const result = matchIrTopLevelPointer(pointer, kind as IrTopLevelKind);
5151+ const result = matchIrPointerToGroup(pointer, kind as IrTopLevelKind);
6052 expect(result.matched).toBe(expected.matched);
6153 if (expected.matched) {
6254 expect(result.kind).toBe(expected.kind);
···6658 });
6759 }
6860});
6969-7070-describe('walkTopological', () => {
7171- const makeGraph = (
7272- deps: Array<[string, Array<string>]>,
7373- nodes: Array<string>,
7474- ) => {
7575- const nodeDependencies = new Map<string, Set<string>>();
7676- const subtreeDependencies = new Map<string, Set<string>>();
7777- const reverseNodeDependencies = new Map<string, Set<string>>();
7878- const nodesMap = new Map<string, any>();
7979-8080- for (const name of nodes) {
8181- nodesMap.set(name, { key: null, node: {}, parentPointer: null });
8282- }
8383-8484- for (const [from, toList] of deps) {
8585- const s = new Set<string>(toList);
8686- nodeDependencies.set(from, s);
8787- subtreeDependencies.set(from, new Set<string>(toList));
8888- for (const to of toList) {
8989- if (!reverseNodeDependencies.has(to))
9090- reverseNodeDependencies.set(to, new Set());
9191- reverseNodeDependencies.get(to)!.add(from);
9292- }
9393- }
9494-9595- return {
9696- nodeDependencies,
9797- nodes: nodesMap,
9898- reverseNodeDependencies,
9999- subtreeDependencies,
100100- transitiveDependencies: new Map<string, Set<string>>(),
101101- } as unknown as Graph;
102102- };
103103-104104- it('walks nodes in topological order for a simple acyclic graph', () => {
105105- // Graph: A -> B -> C
106106- const graph = makeGraph(
107107- [
108108- ['A', ['B']],
109109- ['B', ['C']],
110110- ],
111111- ['A', 'B', 'C'],
112112- );
113113- const order: Array<string> = [];
114114- walkTopological(graph, (pointer) => order.push(pointer));
115115- expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
116116- expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
117117- expect(order).toHaveLength(3);
118118- });
119119-120120- it('walks nodes in topological order for multiple roots', () => {
121121- // Graph: A -> B, C -> D
122122- const graph = makeGraph(
123123- [
124124- ['A', ['B']],
125125- ['C', ['D']],
126126- ],
127127- ['A', 'B', 'C', 'D'],
128128- );
129129- const order: Array<string> = [];
130130- walkTopological(graph, (pointer) => order.push(pointer));
131131- expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
132132- expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
133133- expect(order).toHaveLength(4);
134134- });
135135-136136- it('walks nodes in topological order for a disconnected graph', () => {
137137- // Graph: A -> B, C (no deps), D (no deps)
138138- const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']);
139139- const order: Array<string> = [];
140140- walkTopological(graph, (pointer) => order.push(pointer));
141141- expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
142142- expect(order).toHaveLength(4);
143143- expect(order).toContain('C');
144144- expect(order).toContain('D');
145145- });
146146-147147- it('walks nodes in topological order for a diamond dependency', () => {
148148- // Graph: A
149149- // / \
150150- // B C
151151- // \ /
152152- // D
153153- const graph = makeGraph(
154154- [
155155- ['A', ['B', 'C']],
156156- ['B', ['D']],
157157- ['C', ['D']],
158158- ],
159159- ['A', 'B', 'C', 'D'],
160160- );
161161- const order: Array<string> = [];
162162- walkTopological(graph, (pointer) => order.push(pointer));
163163- expect(order.indexOf('D')).toBeLessThan(order.indexOf('B'));
164164- expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
165165- expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
166166- expect(order.indexOf('C')).toBeLessThan(order.indexOf('A'));
167167- expect(order).toHaveLength(4);
168168- });
169169-170170- it('walks nodes in topological order for a long chain', () => {
171171- // Graph: A -> B -> C -> D -> E
172172- const graph = makeGraph(
173173- [
174174- ['A', ['B']],
175175- ['B', ['C']],
176176- ['C', ['D']],
177177- ['D', ['E']],
178178- ],
179179- ['A', 'B', 'C', 'D', 'E'],
180180- );
181181- const order: Array<string> = [];
182182- walkTopological(graph, (pointer) => order.push(pointer));
183183- expect(order.indexOf('E')).toBeLessThan(order.indexOf('D'));
184184- expect(order.indexOf('D')).toBeLessThan(order.indexOf('C'));
185185- expect(order.indexOf('C')).toBeLessThan(order.indexOf('B'));
186186- expect(order.indexOf('B')).toBeLessThan(order.indexOf('A'));
187187- expect(order).toHaveLength(5);
188188- });
189189-190190- it('walks all nodes, including cycles', () => {
191191- // Graph: A <-> B (cycle), C (no deps)
192192- const graph = makeGraph(
193193- [
194194- ['A', ['B']],
195195- ['B', ['A']],
196196- ],
197197- ['A', 'B', 'C'],
198198- );
199199- const order: Array<string> = [];
200200- walkTopological(graph, (pointer) => order.push(pointer));
201201- expect(order.sort()).toEqual(['A', 'B', 'C']);
202202- });
203203-204204- it('matches ordering for validators-circular-ref spec', async () => {
205205- const specModule = await import(
206206- '../../../../../specs/3.1.x/validators-circular-ref.json'
207207- );
208208- const spec = specModule.default ?? specModule;
209209- const { graph } = buildGraph(spec, loggerStub);
210210-211211- const order: Array<string> = [];
212212- walkTopological(graph, (pointer) => order.push(pointer));
213213-214214- const foo = '#/components/schemas/Foo';
215215- const bar = '#/components/schemas/Bar';
216216- const baz = '#/components/schemas/Baz';
217217- const qux = '#/components/schemas/Qux';
218218-219219- // Bar should come before Foo because Foo depends on Bar
220220- expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo));
221221-222222- // Baz and Qux form a mutual $ref cycle; both must be present
223223- expect(order).toContain(baz);
224224- expect(order).toContain(qux);
225225- });
226226-227227- it('prefers schema group before parameter when safe (default)', () => {
228228- // parameter then schema in declaration order, no deps -> schema should move before parameter
229229- const param = '#/components/parameters/P';
230230- const schema = '#/components/schemas/A';
231231- const nodes = [param, schema];
232232- const graph = makeGraph([], nodes);
233233-234234- const order: Array<string> = [];
235235- walkTopological(graph, (p) => order.push(p));
236236- expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param));
237237- });
238238-239239- it('does not apply preferGroups when it would violate dependencies (fallback)', () => {
240240- // declaration order: param, schema; schema depends on param -> cannot move before param
241241- const param = '#/components/parameters/P';
242242- const schema = '#/components/schemas/S';
243243- const nodes = [param, schema];
244244- const nodeDependencies = new Map<string, Set<string>>();
245245- nodeDependencies.set(schema, new Set([param]));
246246- const subtreeDependencies = new Map<string, Set<string>>();
247247- const reverseNodeDependencies = new Map<string, Set<string>>();
248248- const nodesMap = new Map<string, any>();
249249- for (const n of nodes)
250250- nodesMap.set(n, { key: null, node: {}, parentPointer: null });
251251- const graph = {
252252- nodeDependencies,
253253- nodes: nodesMap,
254254- reverseNodeDependencies,
255255- subtreeDependencies,
256256- transitiveDependencies: new Map<string, Set<string>>(),
257257- } as unknown as Graph;
258258-259259- const order: Array<string> = [];
260260- walkTopological(graph, (p) => order.push(p));
261261- // schema depends on param so param must remain before schema
262262- expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema));
263263- });
264264-265265- it('ignores self-dependencies when ordering', () => {
266266- // Foo has self-ref only, Bar references Foo -> Foo should come before Bar
267267- const foo = '#/components/schemas/Foo';
268268- const bar = '#/components/schemas/Bar';
269269- const nodes = [foo, bar];
270270- const nodeDependencies = new Map<string, Set<string>>();
271271- nodeDependencies.set(foo, new Set([foo]));
272272- nodeDependencies.set(bar, new Set([foo]));
273273-274274- const nodesMap = new Map<string, any>();
275275- for (const n of nodes)
276276- nodesMap.set(n, { key: null, node: {}, parentPointer: null });
277277-278278- const graph = {
279279- nodeDependencies,
280280- nodes: nodesMap,
281281- reverseNodeDependencies: new Map<string, Set<string>>(),
282282- subtreeDependencies: new Map<string, Set<string>>(),
283283- transitiveDependencies: new Map<string, Set<string>>(),
284284- } as unknown as Graph;
285285-286286- const order: Array<string> = [];
287287- walkTopological(graph, (p) => order.push(p));
288288- // Foo is a dependency of Bar, so Foo should come before Bar
289289- expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar));
290290- });
291291-292292- it('uses subtreeDependencies when nodeDependencies are absent', () => {
293293- const parent = '#/components/schemas/Parent';
294294- const child = '#/components/schemas/Child';
295295- const nodes = [parent, child];
296296- const nodeDependencies = new Map<string, Set<string>>();
297297- const subtreeDependencies = new Map<string, Set<string>>();
298298- subtreeDependencies.set(parent, new Set([child]));
299299-300300- const nodesMap = new Map<string, any>();
301301- for (const n of nodes)
302302- nodesMap.set(n, { key: null, node: {}, parentPointer: null });
303303-304304- const graph = {
305305- nodeDependencies,
306306- nodes: nodesMap,
307307- reverseNodeDependencies: new Map<string, Set<string>>(),
308308- subtreeDependencies,
309309- transitiveDependencies: new Map<string, Set<string>>(),
310310- } as unknown as Graph;
311311-312312- const order: Array<string> = [];
313313- walkTopological(graph, (p) => order.push(p));
314314- expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent));
315315- });
316316-317317- it('preserves declaration order for equal-priority items (stability)', () => {
318318- const a = '#/components/schemas/A';
319319- const b = '#/components/schemas/B';
320320- const c = '#/components/schemas/C';
321321- const nodes = [a, b, c];
322322- const graph = makeGraph([], nodes);
323323- const order: Array<string> = [];
324324- walkTopological(graph, (p) => order.push(p));
325325- expect(order).toEqual(nodes);
326326- });
327327-});
+1-1
packages/openapi-ts/src/ir/context.ts
···33import type { Package } from '~/config/utils/package';
44import { packageFactory } from '~/config/utils/package';
55import { TypeScriptRenderer } from '~/generate/renderer';
66-import type { Graph } from '~/openApi/shared/utils/graph';
66+import type { Graph } from '~/graph';
77import { buildName } from '~/openApi/shared/utils/name';
88import type { PluginConfigMap } from '~/plugins/config';
99import { PluginInstance } from '~/plugins/shared/utils/instance';
+19-194
packages/openapi-ts/src/ir/graph.ts
···11-import type { Graph, NodeInfo } from '~/openApi/shared/utils/graph';
22-import { MinHeap } from '~/utils/minHeap';
33-44-type KindPriority = Record<IrTopLevelKind, number>;
55-type PreferGroups = ReadonlyArray<IrTopLevelKind>;
66-type PriorityFn = (pointer: string) => number;
77-88-/**
99- * Walks the nodes of the graph in topological order (dependencies before dependents).
1010- * Calls the callback for each node pointer in order.
1111- * Nodes in cycles are grouped together and emitted in arbitrary order within the group.
1212- *
1313- * @param graph - The dependency graph
1414- * @param callback - Function to call for each node pointer
1515- */
1616-export const walkTopological = (
1717- graph: Graph,
1818- callback: (pointer: string, nodeInfo: NodeInfo) => void,
1919- options?: {
2020- preferGroups?: PreferGroups;
2121- priority?: PriorityFn;
2222- },
2323-) => {
2424- // Stable Kahn's algorithm that respects declaration order as a tiebreaker.
2525- const pointers = Array.from(graph.nodes.keys());
2626- // Base insertion order
2727- const baseIndex = new Map<string, number>();
2828- pointers.forEach((pointer, index) => baseIndex.set(pointer, index));
2929-3030- // Composite decl index: group priority then base insertion order
3131- const declIndex = new Map<string, number>();
3232- const priorityFn = options?.priority ?? defaultPriorityFn;
3333- for (const pointer of pointers) {
3434- const group = priorityFn(pointer) ?? 10;
3535- const composite = group * 1_000_000 + (baseIndex.get(pointer) ?? 0);
3636- declIndex.set(pointer, composite);
3737- }
3838-3939- // Build dependency sets for each pointer (prefer subtreeDependencies, fall back to nodeDependencies)
4040- const depsOf = new Map<string, Set<string>>();
4141- for (const pointer of pointers) {
4242- const raw =
4343- graph.subtreeDependencies?.get(pointer) ??
4444- graph.nodeDependencies?.get(pointer) ??
4545- new Set();
4646- const filtered = new Set<string>();
4747- for (const rawPointer of raw) {
4848- if (rawPointer === pointer) continue; // ignore self-dependencies for ordering
4949- if (graph.nodes.has(rawPointer)) {
5050- filtered.add(rawPointer);
5151- }
5252- }
5353- depsOf.set(pointer, filtered);
5454- }
5555-5656- // Build inDegree and dependents adjacency
5757- const inDegree = new Map<string, number>();
5858- const dependents = new Map<string, Set<string>>();
5959- for (const pointer of pointers) {
6060- inDegree.set(pointer, 0);
6161- }
6262- for (const [pointer, deps] of depsOf) {
6363- inDegree.set(pointer, deps.size);
6464- for (const d of deps) {
6565- if (!dependents.has(d)) {
6666- dependents.set(d, new Set());
6767- }
6868- dependents.get(d)!.add(pointer);
6969- }
7070- }
7171-7272- // Helper to sort pointers by declaration order
7373- const sortByDecl = (arr: Array<string>) =>
7474- arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!);
7575-7676- // Initialize queue with zero-inDegree nodes in declaration order
7777- // Use a small binary min-heap prioritized by declaration index to avoid repeated full sorts.
7878- const heap = new MinHeap(declIndex);
7979- for (const pointer of pointers) {
8080- if ((inDegree.get(pointer) ?? 0) === 0) {
8181- heap.push(pointer);
8282- }
8383- }
8484-8585- const emitted = new Set<string>();
8686- const order: Array<string> = [];
8787-8888- while (!heap.isEmpty()) {
8989- const cur = heap.pop()!;
9090- if (emitted.has(cur)) continue;
9191- emitted.add(cur);
9292- order.push(cur);
9393-9494- const deps = dependents.get(cur);
9595- if (!deps) continue;
9696-9797- for (const dep of deps) {
9898- const v = (inDegree.get(dep) ?? 0) - 1;
9999- inDegree.set(dep, v);
100100- if (v === 0) {
101101- heap.push(dep);
102102- }
103103- }
104104- }
105105-106106- // emit remaining nodes (cycles) in declaration order
107107- const remaining = pointers.filter((pointer) => !emitted.has(pointer));
108108- sortByDecl(remaining);
109109- for (const pointer of remaining) {
110110- emitted.add(pointer);
111111- order.push(pointer);
112112- }
113113-114114- // prefer specified groups when safe
115115- let finalOrder = order;
116116- const preferGroups = options?.preferGroups ?? defaultPreferGroups;
117117- if (preferGroups && preferGroups.length > 0) {
118118- // build group priority map (lower = earlier)
119119- const groupPriority = new Map<string, number>();
120120- for (let i = 0; i < preferGroups.length; i++) {
121121- const k = preferGroups[i];
122122- if (k) {
123123- groupPriority.set(k, i);
124124- }
125125- }
126126-127127- const getGroup: PriorityFn = (pointer) => {
128128- const result = matchIrTopLevelPointer(pointer);
129129- if (result.matched) {
130130- return groupPriority.has(result.kind)
131131- ? groupPriority.get(result.kind)!
132132- : preferGroups.length;
133133- }
134134- return preferGroups.length;
135135- };
136136-137137- // proposed order: sort by (groupPriority, originalIndex)
138138- const proposed = [...order].sort((a, b) => {
139139- const ga = getGroup(a);
140140- const gb = getGroup(b);
141141- return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b);
142142- });
143143-144144- // Build quick lookup of original index and proposed index
145145- const proposedIndex = new Map<string, number>();
146146- for (let i = 0; i < proposed.length; i++) {
147147- proposedIndex.set(proposed[i]!, i);
148148- }
149149-150150- // Micro-optimization: only validate edges where group(dep) > group(node)
151151- const violated = (() => {
152152- for (const [node, deps] of depsOf) {
153153- for (const dep of deps) {
154154- const gDep = getGroup(dep);
155155- const gNode = getGroup(node);
156156- if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping
157157- const pDep = proposedIndex.get(dep)!;
158158- const pNode = proposedIndex.get(node)!;
159159- if (pDep >= pNode) {
160160- return true;
161161- }
162162- }
163163- }
164164- return false;
165165- })();
166166-167167- if (!violated) {
168168- finalOrder = proposed;
169169- }
170170- }
171171-172172- // Finally, call back in final order
173173- for (const pointer of finalOrder) {
174174- callback(pointer, graph.nodes.get(pointer)!);
175175- }
176176-};
11+import type { GetPointerPriorityFn, PointerGroupMatch } from '~/graph';
17721783export const irTopLevelKinds = [
1794 'operation',
···18510] as const;
1861118712export type IrTopLevelKind = (typeof irTopLevelKinds)[number];
188188-189189-export type IrTopLevelPointerMatch =
190190- | { kind: IrTopLevelKind; matched: true }
191191- | { kind?: undefined; matched: false };
1921319314/**
19415 * Checks if a pointer matches a known top-level IR component (schema, parameter, etc) and returns match info.
···19718 * @param kind - (Optional) The component kind to check
19819 * @returns { matched: true, kind: IrTopLevelKind } | { matched: false } - Whether it matched, and the matched kind if so
19920 */
200200-export const matchIrTopLevelPointer = (
2121+export const matchIrPointerToGroup = (
20122 pointer: string,
20223 kind?: IrTopLevelKind,
203203-): IrTopLevelPointerMatch => {
2424+): PointerGroupMatch<IrTopLevelKind> => {
20425 const patterns: Record<IrTopLevelKind, RegExp> = {
20526 operation:
20627 /^#\/paths\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/,
···22647};
2274822849// default grouping preference (earlier groups emitted first when safe)
229229-export const defaultPreferGroups = [
5050+export const preferGroups = [
5151+ 'server',
23052 'schema',
23153 'parameter',
23254 'requestBody',
23355 'operation',
234234- 'server',
23556 'webhook',
236236-] satisfies PreferGroups;
5757+] satisfies ReadonlyArray<IrTopLevelKind>;
5858+5959+type KindPriority = Record<IrTopLevelKind, number>;
2376023861// default group priority (lower = earlier)
239239-// built from `defaultPreferGroups` so the priority order stays in sync with the prefer-groups array.
240240-const defaultKindPriority: KindPriority = (() => {
6262+// built from `preferGroups` so the priority order stays in sync with the prefer-groups array.
6363+const kindPriority: KindPriority = (() => {
24164 const partial: Partial<KindPriority> = {};
242242- for (let i = 0; i < defaultPreferGroups.length; i++) {
243243- const k = defaultPreferGroups[i];
6565+ for (let i = 0; i < preferGroups.length; i++) {
6666+ const k = preferGroups[i];
24467 if (k) partial[k] = i;
24568 }
24669 // Ensure all known kinds exist in the map (fall back to a high index).
24770 for (const k of irTopLevelKinds) {
24871 if (partial[k] === undefined) {
249249- partial[k] = defaultPreferGroups.length;
7272+ partial[k] = preferGroups.length;
25073 }
25174 }
25275 return partial as KindPriority;
25376})();
25477255255-const defaultPriorityFn: PriorityFn = (pointer) => {
256256- const result = matchIrTopLevelPointer(pointer);
7878+const defaultPriority = 10;
7979+8080+export const getIrPointerPriority: GetPointerPriorityFn = (pointer) => {
8181+ const result = matchIrPointerToGroup(pointer);
25782 if (result.matched) {
258258- return defaultKindPriority[result.kind] ?? 10;
8383+ return kindPriority[result.kind] ?? defaultPriority;
25984 }
260260- return 10;
8585+ return defaultPriority;
26186};
···11+import type { Graph } from '~/graph';
12import { createOperationKey } from '~/ir/operation';
23import type { Logger } from '~/utils/logger';
34import { jsonPointerToPath } from '~/utils/ref';
4556import { addNamespace, stringToNamespace } from '../utils/filter';
66-import type { Graph } from '../utils/graph';
77import { httpMethods } from '../utils/operation';
8899export type ResourceMetadata = {
···11+import type { Graph } from '~/graph';
12import type { Logger } from '~/utils/logger';
23import { jsonPointerToPath } from '~/utils/ref';
3445import type { Config } from '../../../types/config';
56import deepEqual from '../utils/deepEqual';
66-import { buildGraph, type Graph, type Scope } from '../utils/graph';
77+import { buildGraph, type Scope } from '../utils/graph';
78import { buildName } from '../utils/name';
89import { deepClone } from '../utils/schema';
910import { childSchemaRelationships } from '../utils/schemaChildRelationships';
···11+import type { Graph, NodeInfo } from '~/graph';
12import type { Logger } from '~/utils/logger';
23import { normalizeJsonPointer, pathToJsonPointer } from '~/utils/ref';
34···1011 * - 'write': Node is write-only (e.g., writeOnly: true).
1112 */
1213export type Scope = 'normal' | 'read' | 'write';
1313-1414-/**
1515- * Information about a node in the OpenAPI graph.
1616- *
1717- * @property deprecated - Whether the node is deprecated. Optional.
1818- * @property key - The property name or array index in the parent, or null for root.
1919- * @property node - The actual object at this pointer in the spec.
2020- * @property parentPointer - The JSON Pointer of the parent node, or null for root.
2121- * @property scopes - The set of access scopes for this node, if any. Optional.
2222- * @property tags - The set of tags for this node, if any. Optional.
2323- */
2424-export type NodeInfo = {
2525- /** Whether the node is deprecated. Optional. */
2626- deprecated?: boolean;
2727- /** The property name or array index in the parent, or null for root. */
2828- key: string | number | null;
2929- /** The actual object at this pointer in the spec. */
3030- node: unknown;
3131- /** The JSON Pointer of the parent node, or null for root. */
3232- parentPointer: string | null;
3333- /** The set of access scopes for this node, if any. Optional. */
3434- scopes?: Set<Scope>;
3535- /** The set of tags for this node, if any. Optional. */
3636- tags?: Set<string>;
3737-};
3838-3939-/**
4040- * The main graph structure for OpenAPI node analysis.
4141- *
4242- * @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.
4343- * @property nodes - Map from normalized JSON Pointer to NodeInfo for every node in the spec.
4444- * @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.
4545- */
4646-export type Graph = {
4747- /**
4848- * For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref.
4949- * Nodes with no dependencies are omitted from this map.
5050- */
5151- nodeDependencies: Map<string, Set<string>>;
5252- /**
5353- * Map from normalized JSON Pointer to NodeInfo for every node in the spec.
5454- */
5555- nodes: Map<string, NodeInfo>;
5656- /**
5757- * For each node with at least one dependent, the set of nodes that reference it via $ref.
5858- * Nodes with no dependents are omitted from this map.
5959- */
6060- reverseNodeDependencies: Map<string, Set<string>>;
6161- /**
6262- * For each node, the set of direct $ref targets that appear anywhere inside the node's
6363- * subtree (the node itself and its children). This is populated during graph construction
6464- * and is used to compute top-level dependency relationships where $ref may be attached to
6565- * child pointers instead of the parent.
6666- */
6767- subtreeDependencies: Map<string, Set<string>>;
6868- /**
6969- * For each node, the set of all (transitive) normalized JSON Pointers it references via $ref anywhere in its subtree.
7070- * This includes both direct and indirect dependencies, making it useful for filtering, codegen, and tree-shaking.
7171- */
7272- transitiveDependencies: Map<string, Set<string>>;
7373-};
74147515/**
7616 * Ensures every relevant child node (e.g., properties, items) in the graph has a `scopes` property.
···4747 WalkEvents,
4848 { type: T }
4949>;
5050-5151-export type WalkOptions = {
5252- /**
5353- * Order of walking schemas.
5454- *
5555- * The "declarations" option ensures that schemas are walked in the order
5656- * they are declared in the input document. This is useful for scenarios where
5757- * the order of declaration matters, such as when generating code that relies
5858- * on the sequence of schema definitions.
5959- *
6060- * The "topological" option ensures that schemas are walked in an order
6161- * where dependencies are visited before the schemas that depend on them.
6262- * This is useful for scenarios where you need to process or generate
6363- * schemas in a way that respects their interdependencies.
6464- *
6565- * @default 'topological'
6666- */
6767- order?: 'declarations' | 'topological';
6868-};