Skip to content

Commit 4915b96

Browse files
authored
docs: basic documentation of Walker and Tree (stoplightio#2)
1 parent 8441126 commit 4915b96

File tree

4 files changed

+210
-85
lines changed

4 files changed

+210
-85
lines changed

README.md

+8-81
Original file line numberDiff line numberDiff line change
@@ -20,96 +20,20 @@ yarn add @stoplight/json-schema-tree
2020
### Usage
2121

2222
```js
23-
import { SchemaTree } from '@stoplight/json-schema-tree';
23+
import { SchemaTree, SchemaNodeKind, isRegularNode } from '@stoplight/json-schema-tree';
2424

2525
const tree = new SchemaTree(mySchema);
26+
const ALLOWED_DEPTH = 2;
2627

27-
const snapshots = [];
28-
let allowedDepth = 2;
29-
30-
tree.walker.hookInto('stepIn', node => tree.walker.depth >= allowedDepth);
28+
tree.walker.hookInto('stepIn', node => tree.walker.depth <= ALLOWED_DEPTH); // if flattening is needed, this might need to be tweaked to account for the scenarios where certain nodes can be merged (i.e. arrays)
3129

3230
tree.walker.hookInto('filter', node => {
33-
return !!node.type?.includes('integer'); // if a schema property is of type integer, it won't be included in the tree
34-
});
35-
36-
tree.walker.on('enterNode', node => {
37-
// new node in fragment is about to be processed
38-
});
39-
40-
tree.walker.on('stepIn', node => {
41-
// node has some children we'll process
42-
});
43-
44-
tree.walker.on('stepOver', node => {
45-
// a node was skipped
46-
});
47-
48-
tree.walker('exitNode', node => {
49-
// node processed
31+
return !isRegularNode(node) || node.types === null || !node.types.includes(SchemaNodeKind.Integer); // if a schema property is of type integer, it won't be included in the tree
5032
});
5133

5234
tree.populate();
5335

5436
tree.root; // populated tree
55-
56-
allowedDepth++;
57-
tree.invokeWalker(tree.walker.resume(snapshots[0])); // resumes, useful for jsv (expand)
58-
```
59-
60-
#### General tree structure
61-
62-
The tree self expands if a schema we build tree for has circular $refs.
63-
64-
Example
65-
66-
```ts
67-
const schema: JSONSchema4 = {
68-
type: 'object',
69-
properties: {
70-
foo: {
71-
type: 'array',
72-
items: {
73-
type: 'object',
74-
properties: {
75-
user: {
76-
$ref: '#/properties/bar',
77-
},
78-
},
79-
},
80-
},
81-
bar: {
82-
$ref: '#/properties/baz',
83-
},
84-
baz: {
85-
$ref: '#/properties/foo',
86-
},
87-
},
88-
};
89-
90-
const tree = new SchemaTree(schema);
91-
tree.populate();
92-
93-
expect(tree.root.children[0].children[0].children[0].children[0].children[0].children[0].path).toEqual([
94-
'properties',
95-
'foo',
96-
'items',
97-
'properties',
98-
'user',
99-
'items',
100-
'properties',
101-
'user',
102-
]);
103-
expect(tree.root.children[0].children[2].children[0].children[0].children[0].children[0].path).toEqual([
104-
'properties',
105-
'baz',
106-
'items',
107-
'properties',
108-
'user',
109-
'items',
110-
'properties',
111-
'user',
112-
]);
11337
```
11438

11539
### Contributing
@@ -120,6 +44,9 @@ expect(tree.root.children[0].children[2].children[0].children[0].children[0].chi
12044
4. Make your changes.
12145
5. Run tests: `yarn test.prod`.
12246
6. Stage relevant files to git.
123-
7. Commit: `yarn commit`. _NOTE: Commits that don't follow the [conventional](https://github.com/marionebl/commitlint/tree/master/%40commitlint/config-conventional) format will be rejected. `yarn commit` creates this format for you, or you can put it together manually and then do a regular `git commit`._
47+
7. Commit: `yarn commit`. _NOTE: Commits that don't follow the
48+
[conventional](https://github.com/marionebl/commitlint/tree/master/%40commitlint/config-conventional) format will be
49+
rejected. `yarn commit` creates this format for you, or you can put it together manually and then do a regular
50+
`git commit`._
12451
8. Push: `git push`.
12552
9. Open PR targeting the `master` branch.

docs/tree.md

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Tree
2+
3+
Currently, the tree may contain up to 5 kinds of nodes.
4+
5+
- RootNode - a single tree cannot have more than a single instance of RootNode
6+
- RegularNode and MirroredRegularNode - used for everything other than a schema fragment containing a `$ref`
7+
- ReferenceNode and MirroredReferenceNode - used only for fragments with a `$ref` included
8+
9+
## Mirroring - mirrored nodes
10+
11+
Although the name might sound a bit odd, it is exactly what these nodes are. They literally represent their standard
12+
variant. Mirrored nodes are used to represent the same portion of schema fragment. However, there are properties that do
13+
vary. These are `id` and `children`. Each node has always unique `id`, regardless of their type. Moreover, to avoid
14+
entering some infinite loop, `children` (if any) are processed shallowly.
15+
16+
For a more end-to-end example, I encourage you to execute the sample provided below in [circular $refs](#circular-refs)
17+
section.
18+
19+
### Caution
20+
21+
**DO NOT** use `instanceof` checks for verifying whether a particular node is of a regular kind. Each regular node has
22+
`Symbol.hasInstance` trait implemented that will return true if you use a regular or mirrored sort of node on the RHS of
23+
condition.
24+
25+
```
26+
myNode instanceof RegularNode; // DO NOT DO IT
27+
28+
if (!isMirroredNode(myNode)) {
29+
// this is okay
30+
}
31+
```
32+
33+
Do note that the fact a mirrored node exists in tree does not necessarily mean the tree has no boundaries. Mirrored
34+
nodes are also used for fragments that were already processed previously.
35+
36+
## $refs
37+
38+
### Resolving
39+
40+
By default, we attempt to resolve all local $refs leveraging `resolveInlineRef` util from `@stoplight/json`. If you wish
41+
to provide a custom resolver, you can supply a `resolver` option to a tree.
42+
43+
```js
44+
import { SchemaTree } from '@stoplight/json-schema-tree';
45+
46+
const tree = new SchemaTree(schema, {
47+
refResolver(ref, propertyPath, fragment) {
48+
// ref has a pointer and a source
49+
// do something or throw if resolving cannot be performed
50+
},
51+
});
52+
```
53+
54+
Retaining all $refs in tree is possible as well. In such case, you should pass `null` as the value of `refResolver`.
55+
56+
```js
57+
import { SchemaTree } from '@stoplight/json-schema-tree';
58+
59+
const tree = new SchemaTree(schema, {
60+
refResolver: null, // I will not try to resolve any $refs
61+
});
62+
```
63+
64+
It is worth mentioning that we do not resolve the whole schema upfront. $refs are resolved only when they are actually
65+
seen (processed) during the traversal.
66+
67+
### Circular $refs
68+
69+
If your schema contains circular $refs, certain branches of tree will have mirrored nodes as some of their leaf nodes.
70+
That said, the size of the tree will be infinite.
71+
72+
#### Example schema with indirect circular references
73+
74+
```json
75+
{
76+
"type": "object",
77+
"properties": {
78+
"foo": {
79+
"type": "array",
80+
"items": {
81+
"type": "object",
82+
"properties": {
83+
"user": {
84+
"$ref": "#/properties/bar"
85+
}
86+
}
87+
}
88+
},
89+
"bar": {
90+
"$ref": "#/properties/baz"
91+
},
92+
"baz": {
93+
"$ref": "#/properties/foo"
94+
}
95+
}
96+
}
97+
```
98+
99+
```js
100+
import { SchemaTree } from '@stoplight/json-schema-tree';
101+
102+
const tree = new SchemaTree(schema);
103+
tree.populate();
104+
105+
expect(tree.root.children[0].children[0].children[0].children[0].children[0].children[0].path).toEqual([
106+
'properties',
107+
'foo',
108+
'items',
109+
'properties',
110+
'user',
111+
'items',
112+
'properties',
113+
'user',
114+
]);
115+
expect(tree.root.children[0].children[2].children[0].children[0].children[0].children[0].path).toEqual([
116+
'properties',
117+
'baz',
118+
'items',
119+
'properties',
120+
'user',
121+
'items',
122+
'properties',
123+
'user',
124+
]);
125+
126+
// etc.
127+
```
128+
129+
**CAUTION**
130+
131+
Always use `isMirroredNode` guard when traversing the processed tree. If you forget to do it, you will enter recursive
132+
loops at times.
133+
134+
```js
135+
function traverse(node) {
136+
if (isMirroredNode(node)) {
137+
// alright, it's a mirrored node, I can do something with it
138+
} else {
139+
// continue
140+
}
141+
}
142+
```

docs/walker.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Walker
2+
3+
Walker is responsible for traversing the schema and fills the tree with nodes.
4+
5+
A good example of robust integration can be found
6+
[here](https://github.com/stoplightio/json-schema-viewer/blob/4cc585424390459bf27c41e2818343a6d5bf249e/src/tree/tree.ts).
7+
8+
## Hooks
9+
10+
- `filter` - can be leveraged in case you do not care about certain kinds of schemas, i.e. anyOf or oneOf in OAS2.
11+
- `stepIn` - can be used if you do not wish to enter a given child. Useful for JSV and alike when you want to process N
12+
level of tree.
13+
14+
## Events
15+
16+
Walker emits a number of events to
17+
18+
```ts
19+
export type WalkerNodeEventHandler = (node: SchemaNode) => void;
20+
export type WalkerFragmentEventHandler = (node: SchemaFragment) => void;
21+
export type WalkerErrorEventHandler = (ex: Error) => void;
22+
23+
export type WalkerEmitter = {
24+
enterNode: WalkerNodeEventHandler; // emitted right after a node is created
25+
exitNode: WalkerNodeEventHandler; // emitted when a given node is fully processed
26+
27+
includeNode: WalkerNodeEventHandler; // emitted when filter hook does not exist or returns a true value
28+
skipNode: WalkerNodeEventHandler; // emitted when filter hook returns false value and therefore node is skipped
29+
30+
stepInNode: WalkerNodeEventHandler; // dispatched when we step into a given node (i.e object, array, or combiner)
31+
stepOutNode: WalkerNodeEventHandler; // emitted when we are on the top level again
32+
stepOverNode: WalkerNodeEventHandler; // emitted when stepIn hook returned a false value
33+
34+
enterFragment: WalkerFragmentEventHandler; // dispatched when a given schema fragment is about to processed
35+
exitFragment: WalkerFragmentEventHandler; // dispatched when a particular schema fragment is fully processed
36+
37+
error: WalkerErrorEventHandler; // any meaningful error such as allOf merging error or resolving error
38+
};
39+
```
40+
41+
## Expanding
42+
43+
```js
44+
// Make sure that both `filter` and `stepIn` hook do not intefere.
45+
46+
tree.walker.restoreWalkerAtNode(someNodeToExpand);
47+
tree.populate();
48+
```

package.json

+12-4
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
"scripts": {
2323
"build": "sl-scripts bundle --sourcemap",
2424
"commit": "git-cz",
25-
"lint": "eslint 'src/**/*.{ts,tsx}'",
26-
"lint.fix": "yarn lint --fix",
25+
"lint": "yarn lint.prettier && yarn lint.eslint",
26+
"lint.fix": "yarn lint.prettier --write && yarn lint.eslint --fix",
27+
"lint.eslint": "eslint --cache --cache-location .cache/ --ext=.js,.ts src",
28+
"lint.prettier": "prettier --ignore-path .eslintignore --check docs/**/*.md README.md",
2729
"release": "sl-scripts release",
2830
"release.dryRun": "sl-scripts release --dry-run --debug",
2931
"test": "jest",
@@ -65,8 +67,14 @@
6567
"typescript": "^4.1.2"
6668
},
6769
"lint-staged": {
68-
"*.{ts,tsx}$": [
69-
"yarn lint.fix"
70+
"*.{ts,tsx}": [
71+
"eslint --fix --cache --cache-location .cache"
72+
],
73+
"docs/**/*.md": [
74+
"prettier --ignore-path .eslintignore --write"
75+
],
76+
"README.md": [
77+
"prettier --write"
7078
]
7179
},
7280
"husky": {

0 commit comments

Comments
 (0)