Skip to content

Commit 9976da8

Browse files
committed
feat(walker): add hooks & more events
1 parent c5bc0d3 commit 9976da8

File tree

3 files changed

+111
-81
lines changed

3 files changed

+111
-81
lines changed

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,24 @@ const tree = new SchemaTree(mySchema);
2626
const snapshots = [];
2727
let allowedDepth = 4;
2828

29-
tree.walker.on('enter', node => {
30-
if (tree.walker.depth >= allowedDepth) {
31-
tree.walker.stepIn = false;
32-
snapshots.push(tree.walker.pause());
33-
}
29+
tree.walker.hookInto('stepIn', node => {
30+
return tree.walker.depth >= allowedDepth;
31+
});
32+
33+
tree.walker.hookInto('filter', node => {
34+
return !!node.type?.includes('integeer'); // sorry, we don't care about integers
35+
});
36+
37+
tree.walker.on('newNode', node => {
38+
// new node in fragment is about to be processed
39+
});
40+
41+
tree.walker.on('enterNode', node => {
42+
// node has some children we'll process
43+
});
44+
45+
tree.walker('exitNode', node => {
46+
// node processed
3447
});
3548

3649
tree.populate();

src/walker/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SchemaNode } from '../nodes';
12
import type { SchemaFragment } from '../types';
23

34
export type WalkerRefResolver = (path: string[] | null, $ref: string) => SchemaFragment;
@@ -6,3 +7,20 @@ export type WalkingOptions = {
67
mergeAllOf: boolean;
78
resolveRef: WalkerRefResolver | null;
89
};
10+
11+
export type WalkerItem = {
12+
node: SchemaNode;
13+
parentNode: SchemaNode | null;
14+
};
15+
16+
export type WalkerSnapshot = {
17+
readonly fragment: SchemaFragment;
18+
readonly depth: number;
19+
readonly path: string[];
20+
};
21+
22+
export type WalkerHookAction = 'filter' | 'stepIn';
23+
export type WalkerHookHandler = (node: SchemaNode) => boolean;
24+
25+
export type WalkerEvent = 'newNode' | 'enterNode' | 'exitNode';
26+
export type WalkerEventHandler = (node: SchemaNode) => void;

src/walker/walk.ts

Lines changed: 75 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventEmitter } from '@stoplight/lifecycle';
2+
import type { Dictionary } from '@stoplight/types';
23

34
import { mergeAllOf } from '../mergers/mergeAllOf';
45
import { mergeOneOrAnyOf } from '../mergers/mergeOneOrAnyOf';
@@ -8,81 +9,25 @@ import type { RootNode } from '../nodes/RootNode';
89
import { SchemaCombinerName, SchemaNode, SchemaNodeKind } from '../nodes/types';
910
import type { SchemaFragment } from '../types';
1011
import { isObjectLiteral } from '../utils/guards';
11-
import type { WalkingOptions } from './types';
12-
13-
function* processFragment(
14-
fragment: SchemaFragment,
15-
path: string[],
16-
walkingOptions: WalkingOptions,
17-
processedFragments: WeakMap<SchemaFragment, SchemaNode>,
18-
): IterableIterator<SchemaNode> {
19-
const processedFragment = processedFragments.get(fragment);
20-
if (processedFragment !== void 0) {
21-
return yield new MirrorNode(processedFragment);
22-
}
23-
24-
if ('$ref' in fragment) {
25-
if (walkingOptions.resolveRef !== null && typeof fragment.$ref === 'string') {
26-
try {
27-
const seenRefs: string[] = [];
28-
while (typeof fragment.$ref === 'string') {
29-
if (seenRefs.includes(fragment.$ref)) {
30-
return yield new ReferenceNode(fragment, null);
31-
}
32-
33-
seenRefs.push(fragment.$ref);
34-
fragment = walkingOptions.resolveRef(path, fragment.$ref);
35-
}
36-
} catch (ex) {
37-
return yield new ReferenceNode(fragment, ex.message);
38-
}
39-
} else {
40-
return yield new ReferenceNode(fragment, null);
41-
}
42-
}
43-
44-
if (walkingOptions.mergeAllOf && SchemaCombinerName.AllOf in fragment) {
45-
try {
46-
fragment = mergeAllOf(fragment, path, walkingOptions);
47-
} catch {
48-
//
49-
}
50-
}
51-
52-
if (SchemaCombinerName.OneOf in fragment || SchemaCombinerName.AnyOf in fragment) {
53-
try {
54-
for (const item of mergeOneOrAnyOf(fragment, path, walkingOptions)) {
55-
yield new RegularNode(item);
56-
}
57-
58-
return;
59-
} catch {
60-
//
61-
}
62-
}
63-
64-
yield new RegularNode(fragment);
65-
}
66-
67-
type WalkerItem = {
68-
node: SchemaNode;
69-
parentNode: SchemaNode | null;
70-
};
71-
72-
type WalkerSnapshot = {
73-
readonly fragment: SchemaFragment;
74-
readonly depth: number;
75-
readonly path: string[];
76-
};
77-
78-
export class Walker extends EventEmitter<any> {
12+
import type {
13+
WalkerEvent,
14+
WalkerEventHandler,
15+
WalkerHookAction,
16+
WalkerHookHandler,
17+
WalkerItem,
18+
WalkerSnapshot,
19+
WalkingOptions,
20+
} from './types';
21+
22+
export class Walker extends EventEmitter<Dictionary<WalkerEventHandler, WalkerEvent>> {
7923
public readonly path: string[];
8024
public depth: number;
8125

8226
protected fragment: SchemaFragment;
8327
protected schemaNode: RegularNode | RootNode;
8428

8529
private readonly processedFragments: WeakMap<SchemaFragment, SchemaNode>;
30+
private readonly hooks: Partial<Dictionary<WalkerHookHandler, WalkerHookAction>>;
8631

8732
constructor(protected readonly root: RootNode, protected readonly walkingOptions: WalkingOptions) {
8833
super();
@@ -92,9 +37,9 @@ export class Walker extends EventEmitter<any> {
9237
this.fragment = root.fragment;
9338
this.schemaNode = root;
9439
this.processedFragments = new WeakMap<SchemaFragment, SchemaNode>();
95-
}
9640

97-
public stepIn: boolean = true;
41+
this.hooks = {};
42+
}
9843

9944
public *resume(snapshot: WalkerSnapshot) {
10045
this.path.splice(0, this.path.length, ...snapshot.path);
@@ -112,23 +57,32 @@ export class Walker extends EventEmitter<any> {
11257
};
11358
}
11459

60+
public hookInto(action: WalkerHookAction, handler: WalkerHookHandler) {
61+
this.hooks[action] = handler;
62+
}
63+
11564
public *walk(): IterableIterator<WalkerItem> {
11665
const {
11766
depth: initialDepth,
11867
schemaNode: initialSchemaNode,
11968
path: { length },
12069
} = this;
12170

122-
for (const schemaNode of processFragment(this.fragment, this.path, this.walkingOptions, this.processedFragments)) {
71+
for (const schemaNode of this.processFragment()) {
72+
super.emit('newNode', schemaNode);
73+
12374
this.processedFragments.set(schemaNode.fragment, schemaNode);
12475

12576
this.fragment = schemaNode.fragment;
12677
this.depth = initialDepth + 1;
12778

128-
super.emit('enter', schemaNode);
79+
const shouldSkipNode = this.hooks.filter?.(schemaNode);
80+
81+
if (shouldSkipNode === true) {
82+
continue;
83+
}
12984

13085
schemaNode.parent = initialSchemaNode;
131-
// @ts-ignore
13286
schemaNode.subpath = this.path.slice(initialSchemaNode.path.length);
13387

13488
if ('children' in initialSchemaNode) {
@@ -143,11 +97,12 @@ export class Walker extends EventEmitter<any> {
14397

14498
this.schemaNode = schemaNode;
14599

146-
if (this.stepIn) {
100+
if (this.hooks.stepIn?.(schemaNode) === false) {
101+
super.emit('enterNode', schemaNode);
147102
yield* this.walkNodeChildren();
148103
}
149104

150-
super.emit('exit');
105+
super.emit('exitNode', schemaNode);
151106
}
152107

153108
this.path.length = length;
@@ -166,7 +121,6 @@ export class Walker extends EventEmitter<any> {
166121
path: { length },
167122
} = this;
168123

169-
// todo: combiner
170124
if (schemaNode.combiners !== null) {
171125
for (const combiner of schemaNode.combiners) {
172126
const items = fragment[combiner];
@@ -177,6 +131,7 @@ export class Walker extends EventEmitter<any> {
177131
i++;
178132
if (!isObjectLiteral(item)) continue;
179133
this.fragment = item;
134+
// todo: spaghetti
180135
this.schemaNode = initialSchemaNode;
181136
this.depth = initialDepth;
182137
this.path.length = length;
@@ -242,4 +197,48 @@ export class Walker extends EventEmitter<any> {
242197

243198
this.schemaNode = schemaNode;
244199
}
200+
201+
protected *processFragment(): IterableIterator<SchemaNode> {
202+
const { walkingOptions, path, processedFragments } = this;
203+
let { fragment } = this;
204+
205+
const processedFragment = processedFragments.get(fragment);
206+
if (processedFragment !== void 0) {
207+
return yield new MirrorNode(processedFragment);
208+
}
209+
210+
if ('$ref' in fragment) {
211+
if (walkingOptions.resolveRef !== null && typeof fragment.$ref === 'string') {
212+
try {
213+
fragment = walkingOptions.resolveRef(path, fragment.$ref);
214+
} catch (ex) {
215+
return yield new ReferenceNode(fragment, ex?.message ?? 'Unknown resolving error');
216+
}
217+
} else {
218+
return yield new ReferenceNode(fragment, null);
219+
}
220+
}
221+
222+
if (walkingOptions.mergeAllOf && SchemaCombinerName.AllOf in fragment) {
223+
try {
224+
fragment = mergeAllOf(fragment, path, walkingOptions);
225+
} catch {
226+
// no the end of the world - we will render raw unprocessed fragment
227+
}
228+
}
229+
230+
if (SchemaCombinerName.OneOf in fragment || SchemaCombinerName.AnyOf in fragment) {
231+
try {
232+
for (const item of mergeOneOrAnyOf(fragment, path, walkingOptions)) {
233+
yield new RegularNode(item);
234+
}
235+
236+
return;
237+
} catch {
238+
// no the end of the world - we will render raw unprocessed fragment
239+
}
240+
}
241+
242+
yield new RegularNode(fragment);
243+
}
245244
}

0 commit comments

Comments
 (0)