Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add traverse function #59

Merged
merged 17 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions benchmarks/object/traverse.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as _ from 'radashi'
import { bench } from 'vitest'

describe('traverse', () => {
const root = {
a: { b: { c: { d: { e: 1 } } } },
}
bench('basic traversal', () => {
_.traverse(root, () => {})
})
})
244 changes: 244 additions & 0 deletions docs/object/traverse.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
---
title: traverse
description: Deeply enumerate an object and any nested objects
---

### Usage

Recursively visit each property of an object (or each element of an array) and its nested objects or arrays. To traverse non-array iterables (e.g. `Map`, `Set`) and class instances, see the [Traversing other objects](#traversing-other-objects) section.

Traversal is performed in a depth-first manner. That means the deepest object will be visited before the last property of the root object.

```ts
import * as _ from 'radashi'

const root = { a: { b: 2 }, c: [1, 2] }

_.traverse(root, (value, key, parent, context) => {
const depth = context.parents.length
console.log(' '.repeat(depth * 2), key, '=>', value)
})
// Logs the following:
// a => { b: 2 }
// b => 2
// c => [1, 2]
// 0 => 1
// 1 => 2
```

**Tip:** Check out the [Advanced](#advanced) section to see what else is possible.

:::tip[Did you know?]

- Sparse arrays don't have their holes visited.
- Circular references are skipped.

:::

## Types

### TraverseVisitor

The `TraverseVisitor` type represents the function passed to `traverse` as its 2nd argument. If you ever need to declare a visitor separate from a `traverse` call, you can do so by declaring a function with this type signature.

```ts
import { TraverseVisitor } from 'radashi'

const visitor: TraverseVisitor = (value, key, parent, context) => {
// ...
}
```

### TraverseContext

Every visit includes a context object typed with `TraverseContext`, which contains the following properties:

- `key`: The current key being visited.
- `parent`: The parent object of the current value.
- `parents`: An array of objects (from parent to child) that the current value is contained by.
- `path`: An array describing the key path to the current value from the root.
- `skip`: A function used for skipping traversal of an object. If no object is provided, the current value is skipped. See [Skipping objects](#skipping-objects) for more details.
- `skipped`: A set of objects that have been skipped.
- `value`: The current value being visited.

:::danger

The `path` and `parents` arrays are mutated by the `traverse` function. If you need to use them after the current visit, you should make a copy.

:::

### TraverseOptions

You may set these options for `traverse` using an object as its 3rd argument.

- `ownKeys`: A function that returns the own enumerable property names of an object.
- `rootNeedsVisit`: A boolean indicating whether the root object should be visited.

See the [Options](#options) section for more details.

## Options

### Traversing all properties

By default, non-enumerable properties and symbol properties are skipped. You can pass in a custom `ownKeys` implementation to control which object properties are visited.

This example shows how `Reflect.ownKeys` can be used to include non-enumerable properties and symbol properties. Note that symbol properties are always traversed last when using `Reflect.ownKeys`.

```ts
import * as _ from 'radashi'

const symbol = Symbol('b')
const root = { [symbol]: 1 }
Object.defineProperty(root, 'a', { value: 2, enumerable: false })

_.traverse(
root,
(value, key) => {
console.log(key, '=>', value)
},
{ ownKeys: Reflect.ownKeys },
)
// Logs the following:
// a => 2
// Symbol(b) => 1
```

### Visiting the root object

By default, your `visitor` callback will never receive the object passed into `traverse`. To override this behavior, set the `rootNeedsVisit` option to true.

When the root object is visited, the `key` will be `null`.

```ts
import * as _ from 'radashi'

const root = { a: 1 }

_.traverse(
root,
(value, key) => {
console.log(key, '=>', value)
},
{ rootNeedsVisit: true },
)
// Logs the following:
// null => { a: 1 }
// a => 1
```

## Advanced

### Traversing other objects

If traversing plain objects and arrays isn't enough, try calling `traverse` from within another `traverse` callback like follows. This takes advantage of the fact that the root object is always traversed.

```ts
import * as _ from 'radashi'

// Note how we're using a named visitor function so it can reference itself.
_.traverse(root, function visitor(value, key, parent, context, options) {
if (value instanceof MyClass) {
return _.traverse(value, visitor, options, context)
}
// TODO: Handle other values as needed.
})
```

If you didn't set any options, the `options` argument can be null:

```ts
return _.traverse(root, visitor, null, context)
```

### Skipping objects

Using the `TraverseContext::skip` method, you can prevent an object from being traversed. By calling `skip()` with no arguments, the current value won't be traversed.

```ts
import * as _ from 'radashi'

const root = {
a: { b: 1 },
c: { d: 2 },
}

_.traverse(root, (value, key, parent, context) => {
console.log(key, '=>', value)

// Skip traversal of the 'a' object.
if (key === 'a') {
context.skip()
}
})
// Logs the following:
// a => { b: 1 }
// c => { d: 2 }
// d => 2
```

You can pass any object to `skip()` to skip traversal of that object.

```ts
import * as _ from 'radashi'

const root = {
a: {
b: {
c: 1,
},
},
}

_.traverse(root, (value, key, parent, context) => {
console.log(key, '=>', value)

// Visit the properties of the current object, but skip any objects nested within.
Object.values(value).forEach(nestedValue => {
if (_.isObject(nestedValue)) {
context.skip(nestedValue)
}
})
})
// Logs the following:
// a => { b: { c: 1 } }
// b => { c: 1 }
```

### Exiting early

If your `visitor` callback returns false, `traverse` will exit early and also return false. This is useful if you found what you wanted, so you don't need to traverse the rest of the objects.

```ts
let found = null
_.traverse(root, value => {
if (isWhatImLookingFor(value)) {
found = value
return false
}
})
```

### Leave callbacks

If your `visitor` callback returns a function, it will be called once `traverse` has visited every visitable property/element within the current object. This is known as a “leave callback”.

Your leave callback can return false to exit traversal early.

```ts
_.traverse({ arr: ['a', 'b'] }, (value, key) => {
if (isArray(value)) {
console.log('start of array')
return () => {
console.log('end of array')
return false
}
} else {
console.log(key, '=>', value)
}
})
// Logs the following:
// start of array
// 0 => 'a'
// 1 => 'b'
// end of array
```
2 changes: 2 additions & 0 deletions src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export * from './object/omit.ts'
export * from './object/pick.ts'
export * from './object/set.ts'
export * from './object/shake.ts'
export * from './object/traverse.ts'
export * from './object/upperize.ts'

export * from './random/draw.ts'
Expand All @@ -103,6 +104,7 @@ export * from './typed/isFloat.ts'
export * from './typed/isFunction.ts'
export * from './typed/isInt.ts'
export * from './typed/isIntString.ts'
export * from './typed/isIterable.ts'
export * from './typed/isMap.ts'
export * from './typed/isNumber.ts'
export * from './typed/isObject.ts'
Expand Down
Loading
Loading