Skip to content

Commit f25d11d

Browse files
authored
feat(traverse): support onEnter/onLeave (#91)
1 parent 98c58f9 commit f25d11d

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

src/__tests__/traverse.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,39 @@ describe('traverse', () => {
2020
traverse(obj, mockFunc);
2121
expect(mockFunc).toHaveBeenCalledTimes(0);
2222
});
23+
24+
it('should support onEnter/onLeave hooks', () => {
25+
const obj = {
26+
foo: {
27+
bar: 'bear',
28+
},
29+
};
30+
31+
const onEnter = jest.fn();
32+
const onLeave = jest.fn();
33+
traverse(obj, {
34+
onEnter,
35+
onLeave,
36+
});
37+
38+
expect(onEnter).toHaveBeenCalledTimes(2);
39+
expect(onEnter).nthCalledWith(1, {
40+
path: [],
41+
value: obj,
42+
});
43+
expect(onEnter).nthCalledWith(2, {
44+
path: ['foo'],
45+
value: obj.foo,
46+
});
47+
48+
expect(onLeave).toHaveBeenCalledTimes(2);
49+
expect(onLeave).nthCalledWith(1, {
50+
path: ['foo'],
51+
value: obj.foo,
52+
});
53+
expect(onLeave).nthCalledWith(2, {
54+
path: [],
55+
value: obj,
56+
});
57+
});
2358
});

src/traverse.ts

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,37 @@
1-
import { JsonPath } from '@stoplight/types';
2-
3-
export const traverse = (
4-
obj: unknown,
5-
func: (opts: { parent: unknown; parentPath: JsonPath; property: string | number; propertyValue: unknown }) => void,
6-
path: JsonPath = [],
7-
) => {
8-
if (!obj || typeof obj !== 'object') return;
9-
10-
for (const i in obj) {
11-
if (!obj.hasOwnProperty(i)) continue;
12-
func({ parent: obj, parentPath: path, property: i, propertyValue: obj[i] });
13-
if (obj[i] && typeof obj[i] === 'object') {
14-
traverse(obj[i], func, path.concat(i));
1+
import { JsonPath, Segment } from '@stoplight/types';
2+
3+
type Hooks = {
4+
onEnter(ctx: Readonly<{ value: object; path: JsonPath }>): void;
5+
onLeave(ctx: Readonly<{ value: object; path: JsonPath }>): void;
6+
onProperty(ctx: Readonly<{ parent: object; parentPath: JsonPath; property: Segment; propertyValue: unknown }>): void;
7+
};
8+
9+
const _traverse = (obj: object, hooks: Partial<Hooks>, path: JsonPath) => {
10+
const ctx = { value: obj, path };
11+
12+
if (hooks.onEnter) {
13+
hooks.onEnter(ctx);
14+
}
15+
16+
for (const i of Object.keys(obj)) {
17+
const value = obj[i];
18+
19+
if (hooks.onProperty) {
20+
hooks.onProperty({ parent: obj, parentPath: path, property: i, propertyValue: value });
1521
}
22+
23+
if (typeof value === 'object' && value !== null) {
24+
_traverse(value, hooks, path.concat(i));
25+
}
26+
}
27+
28+
if (hooks.onLeave) {
29+
hooks.onLeave(ctx);
30+
}
31+
};
32+
33+
export const traverse = (obj: unknown, hooks: Partial<Hooks> | Hooks['onProperty']) => {
34+
if (typeof obj === 'object' && obj !== null) {
35+
_traverse(obj, typeof hooks === 'function' ? { onProperty: hooks } : hooks, []);
1636
}
1737
};

0 commit comments

Comments
 (0)