Contents
Description
Walk is a 0 dependency Javascript/Typescript library for traversing object trees. The library includes:
- Functions for recursive processing of nested object trees and directed graphs
- User defined, type-specific (array/obj/value) callback hooks that get executed during the traversal
- Support for asynchronous callbacks, run either in sequence or in parallel
- Incremental graph traversal through generators, with full async support
- A variety of convenience functions, which double as implementation examples for the library
Installation
npm install walkjs
Quickstart
Below is a simple example of usage, in which we execute a single callback for each node in the object graph. The
callback simply prints metadata about the node. We also add a global filter to exclude any nodes whose value is equal
to 1
.
import { WalkBuilder } from 'walkjs';
const obj = {
'a': 1,
'b': [2, 3],
'c': {'d': 5}
}
function printNode(node: WalkNode){
console.log("obj" + node.getPath(), "=", node.val)
}
new WalkBuilder()
.withSimpleCallback(printNode)
.withGlobalFilter(node => node.val !== 1)
.walk(obj)
outputs:
obj = { a: 1, b: [ 2, 3 ], c: { d: 4 } }
obj["b"] = [ 2, 3 ]
obj["b"][0] = 2
obj["b"][1] = 3
obj["c"] = { d: 4 }
obj["c"]["d"] = 4
Async
Async walks work almost exactly the same as the sync ones, but have an async signature. All callbacks will be awaited, and therefore still run in sequence. For the async versions below, callback functions may either return Promise<void>
or void
;
import {AsyncWalkBuilder} from 'walkjs';
const obj = {
//...
}
async function callApi(node: WalkNode): Promise<void> {
// do some async work here
}
await new AsyncWalkBuilder()
.withSimpleCallback(callApi)
.walk(exampleObject)
See the reference for more details!
Reference
walk(target: any, config: Config<Callback>): void
The primary method for traversing an object and injecting callbacks into the traversal.
walkAsync(target: any, config: Config<AsyncCallback>): Promise<void>
Async version of walk
which returns a promise.
Halting the walk
import {apply, Break} from "walkjs";
// the walk will not process for any nodes after this
apply({}, () => throw new Break())
Throwing an instance of this class within a callback will halt processing completely. This allows for early exit, usually for circular graphs or in cases when you no longer need to continue.
Configuration:
-
rootObjectCallbacks: boolean
: Ignore callbacks for root objects. -
parallelizeAsyncCallbacks: boolean
: (Only applies to async variations). IgnoreexecutionOrder
and run all async callbacks in parallel. Callbacks will still be grouped by position, so this will only apply to callbacks in the same position group. -
runCallbacks: boolean
: Set this tofalse
to skip callbacks completely. -
callbacks: Callback<T>[]
: an array of callback objects. See the Callback section for more information. -
traversalMode: 'depth'|'breadth'
: the mode for traversing the tree. Options aredepth
for depth-first processing andbreadth
for breadth-first processing. -
graphMode: 'finiteTree'|'graph'|'infinite'
: if the object that gets passed in doesn't comply with this configuration setting, an error will occur. Finite trees will error if an object/array reference is encountered more than once, determined by set membership of the WalkNode'sval
. Graphs will only process object/array references one time. Infinite trees will always process nodes -- usethrow new Break()
to end the processing manually. Warning: infinite trees will never complete processing if a callback doesn'tthrow new Break()
.
Config Defaults
const defaultConfig = {
traversalMode: 'depth',
rootObjectCallbacks: true,
runCallbacks: true,
graphMode: 'finiteTree',
parallelizeAsyncCallbacks: false,
callbacks: []
}
Using the builder
An alternative way to configure a walk is to use either the WalkBuilder
or AsyncWalkBuilder
.
Call WalkBuilder.walk(target: any)
to execute the walk with the builder's configuration.
Example:
import { WalkBuilder } from 'walkjs';
const logCallback = (node: WalkNode) => console.log(node);
const myObject = {}
const result = new WalkBuilder()
// runs for every node
.withSimpleCallback(logCallback)
// configured callback
.withCallback({
keyFilters: ['myKey'],
positionFilter: 'postWalk',
nodeTypeFilters: ['object'],
executionOrder: 0,
callback: logCallback
})
// alternative way to configure callbacks
.withConfiguredCallback(logCallback)
.filteredByKeys('key1', 'key2')
.filteredByNodeTypes('object', 'array')
.filteredByPosition('postWalk')
.withFilter(node => !!node.parent)
.withExecutionOrder(1)
.done()
.withGraphMode('graph')
.withTraversalMode('breadth')
.withRunningCallbacks(true)
.withRootObjectCallbacks(true)
// execute the walk
.walk(myObject)
Callbacks
Callbacks are a way to execute custom functionality on certain nodes within our object tree. The general form of a callback object is:
{
executionOrder: 0,
nodeTypeFilters: ['array'],
keyFilters: ['friends'],
positionFilter: 'preWalk',
callback: function(node: NodeType){
// do things here
}
}
Here are the properties you can define in a callback configuration, most of which act as filters:
-
callback: (node: WalkNode) => void
: the actual function to run. Your callback function will be passed a single argument: aWalkNode
object ( see the Nodes section for more detail). succession. If unspecified, the callback will run'preWalk'
. For async functions,callback
may alternatively return aPromise<void>
, in which case it will be awaited. -
executionOrder: number
: an integer value for controlling order of callback operations. Lower values run earlier. If unspecified, the order will default to 0. Callback stacks are grouped by position and property, so the sort will only apply to callbacks in the same grouping. -
filters: ((node: WalkNode) => boolean)[]
: A list of functions which will exclude nodes when the result of the function for that node isfalse
. -
nodeTypeFilters: NodeType[]
: an array of node types to run on. Options are'array'
,'object'
, and'value'
. If unspecified, the callback will run on any node type. -
keyFilters: string[]
: an array of key names to run on. The callback will check the key of the property against this list. If unspecified, the callback will run on any key. -
positionFilter: PositionType
: The position the traversal to run on -- think of this as when it should execute. Options are'preWalk'
(before any list/object is traversed), and'postWalk'
(after any list/object is traversed). You may also supply'both'
. When the walk is run in'breadth'
mode, the only difference here is whether the callback is invoked prior to yielding the node. However when running in'depth'
mode,'postWalk'
callbacks for a node will run after all the callbacks of its children. For example, if our object is{ a: b: { c: 1, d: 2 } }
, we would expect'postWalk'
callbacks to run in the following order:c
,d
,b
,a
.
Nodes
WalkNode
objects represent a single node in the tree, providing metadata about the value, its parents, siblings, and children. Nodes have the following properties:
-
key: string|number
: The key of this property as defined on its parent. For example, if this callback is running on the'weight'
property of aperson
, thekey
would be'weight'
. This will be the numerical index for members in arrays. -
val: any
: The value of the property. To use the above example, the value would be something like183
. -
nodeType: NodeType
: The type of node the property is. PossibleNodeType
are'array' | 'object' | 'value'
. -
isRoot: boolean
: A boolean that is set totrue
if the property is a root object, otherwisefalse
. -
executedCallbacks: Callback[]
: An array of all callback functions that have already run on this property. The current function will not be in the list. -
getPath(pathFormat?: (node: WalkNode) => string)
The path to the value, formatted with the optional formatter passed in. For example, if the variable you're walking is namedmyObject
, the path will look something like["friends"][10]["friends"][2]["name"]
, such that callingmyObject["friends"][10]["friends"][2]["name"]
will return theval
. ThepathFormat
parameter should take a node and return the path segment for only that node; sincegetPath
will automatically prepend the path of the node's parent as well. -
parent: WalkNode
: The node under which the property exists.node.parent
is another instance of node, and will have all the same properties. -
children: WalkNode[]
: A list of all child nodes. -
siblings: WalkNode[]
: A list of all sibling nodes (nodes which share a parent). -
descendants: WalkNode[]
: A list of all descendant nodes (recursively traversing children). -
ancestors: WalkNode[]
: A list of nodes formed by recursively traversing parents back to the root.
Extra functions
Walk has some extra utility functions built-in that you may find useful.
Apply
apply(
target: any,
...callbacks: ((node: NodeType) => void)[]
): void
applyAsync(
target: any,
...callbacks: (((node: NodeType) => void) | ((node: NodeType) => Promise<void>))[]
): Promise<void>
A shorthand version of walk()
that runs the supplied callbacks for all nodes.
Deep copy
deepCopy(target: object) : object
Returns a deep copy of an object, with all array and object references replaced with new objects/arrays.
Compare
compare(
a: any,
b: any,
leavesOnly=false,
formatter: NodePathSegmentFormatter=defaultFormatter
): NodeComparison
This method does a deep comparison between objects a
and b
based on the keys of each node. It returns an array of the following type:
type NodeComparison = {
path: string,
a?: any
b?: any
hasDifference: boolean,
difference?: 'added' | 'removed' | {before: any, after: any}
}
Reduce
reduce(
source: object,
initialValue: T,
fn: (accumulator: T, node: WalkNode) => T
): T
This function accumulates a value of type T, starting with initialValue
, by invoking fn
on each node in source
and adding the result to the accumulated value T.
Running walk as a generator
Behind the scenes, walk
and walkAsync
run as generators (Generator<WalkNode>
and AsyncGenerator<WalkNode>
, respectively). As they step through the object graph, nodes are yielded.
The default walk
/walkAsync
functions coerce the generator to a list before returning. However, you can access the generator directly; simply use the following imports instead:
import {walkStep, walkAsyncStep} from "walkjs";
// sync
for (const node of walkStep(obj, config))
console.log(node);
// async
for await (const node of walkAsyncStep(obj, config))
console.log(node)
preWalk
callbacks are invoked prior to yielding a node, and postWalk
callbacks after.