objwalk 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -290
- package/SPEC.md +383 -0
- package/dist/index.cjs +151 -132
- package/dist/index.d.cts +31 -5
- package/dist/index.d.ts +31 -5
- package/dist/index.js +150 -132
- package/package.json +3 -2
- package/skills/objwalk/SKILL.md +74 -8
package/README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# objwalk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Work with nested JavaScript data without writing the same traversal code again.
|
|
4
|
+
Read and update values by path, reshape API responses, walk objects and arrays,
|
|
5
|
+
and see exactly what changed between two states.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
- **One path syntax** for nested properties and array indexes.
|
|
8
|
+
- **Tools that work together:** pick, set, reshape, walk, copy, merge, and compare.
|
|
9
|
+
- **No runtime dependencies**, with TypeScript declarations and ESM/CommonJS builds.
|
|
6
10
|
|
|
7
|
-
> Coding agent? Load the [objwalk skill](skills/objwalk/SKILL.md).
|
|
11
|
+
> 🤖 Coding agent? Load the [objwalk skill](https://github.com/khalilov/objwalk/blob/main/skills/objwalk/SKILL.md).
|
|
12
|
+
> Need every edge case? Open the [full specification](https://github.com/khalilov/objwalk/blob/main/SPEC.md).
|
|
8
13
|
|
|
9
14
|
## Install
|
|
10
15
|
|
|
@@ -14,354 +19,187 @@ npm install objwalk
|
|
|
14
19
|
|
|
15
20
|
## Usage
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
import { each, equally, extract, flush, map, merge, parse, pick, set, stringify } from 'objwalk'
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## pick
|
|
22
|
-
|
|
23
|
-
Read a value from an object by path.
|
|
22
|
+
Import the tools you need and use paths to reach nested values:
|
|
24
23
|
|
|
25
24
|
```ts
|
|
26
|
-
|
|
27
|
-
user: {
|
|
28
|
-
name: 'Natalia Romanoff',
|
|
29
|
-
email: 'black.widow@example.com',
|
|
30
|
-
},
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
pick(data, 'user.name')
|
|
34
|
-
// 'Natalia Romanoff'
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Pick several keys from the same nested object.
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
pick(data, 'user.{name,email}')
|
|
41
|
-
// { name: 'Natalia Romanoff', email: 'black.widow@example.com' }
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
List keys from a nested object.
|
|
25
|
+
import { pick, set, flush } from 'objwalk'
|
|
45
26
|
|
|
46
|
-
```ts
|
|
47
|
-
pick(data, 'user.{*}')
|
|
48
|
-
// ['name', 'email']
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Read from arrays with an explicit index.
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
27
|
const data = {
|
|
55
|
-
|
|
28
|
+
user: { name: 'Natalia Romanoff' },
|
|
29
|
+
items: [{ title: 'Keyboard', internalId: 'draft-1' }],
|
|
56
30
|
}
|
|
57
31
|
|
|
58
|
-
pick(data, 'items[
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
pick(data, path, {
|
|
69
|
-
inherited: false,
|
|
70
|
-
ignore: ['__proto__', 'prototype', 'constructor'],
|
|
71
|
-
})
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
The options apply to every path segment, array index, and selected key. With `{*}`, ignored
|
|
75
|
-
keys are omitted from the returned list.
|
|
76
|
-
|
|
77
|
-
## set
|
|
78
|
-
|
|
79
|
-
Set a value inside an object by path.
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
const data = {}
|
|
83
|
-
|
|
84
|
-
set(data, 'user.name', 'Natalia')
|
|
85
|
-
// { user: { name: 'Natalia' } }
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Set values inside arrays with an explicit index.
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
const data = {}
|
|
92
|
-
|
|
93
|
-
set(data, 'items[0].title', 'First')
|
|
94
|
-
// { items: [{ title: 'Romanoff' }] }
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
`set` mutates the object passed to it and returns the same object.
|
|
98
|
-
|
|
99
|
-
## flush
|
|
100
|
-
|
|
101
|
-
Delete a property by path using native `delete` semantics.
|
|
102
|
-
|
|
103
|
-
```ts
|
|
104
|
-
const data = { user: { name: 'Ada', role: 'admin' } }
|
|
105
|
-
|
|
106
|
-
flush(data, 'user.name')
|
|
107
|
-
// { user: { role: 'admin' } }
|
|
108
|
-
|
|
109
|
-
flush({}, 'path.to.key') // {}
|
|
110
|
-
flush({ user: null }, 'user.name') // { user: null }
|
|
32
|
+
pick(data, 'items[0].title') // 'Keyboard'
|
|
33
|
+
set(data, 'user.email', 'black.widow@example.com')
|
|
34
|
+
flush(data, ['user.name', 'items[0].internalId'])
|
|
35
|
+
// data is now:
|
|
36
|
+
// {
|
|
37
|
+
// user: { email: 'black.widow@example.com' },
|
|
38
|
+
// items: [{ title: 'Keyboard' }],
|
|
39
|
+
// }
|
|
111
40
|
```
|
|
112
41
|
|
|
113
|
-
`flush
|
|
114
|
-
|
|
115
|
-
containers or remove empty parents. An empty path leaves the object unchanged.
|
|
116
|
-
Only own properties are traversed and deleted; inherited properties are left unchanged.
|
|
42
|
+
`set`, `flush`, and `merge` update their target and return it. Use `clone` first
|
|
43
|
+
when you need an independent copy of supported data.
|
|
117
44
|
|
|
118
|
-
|
|
45
|
+
## Paths at a glance
|
|
119
46
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
47
|
+
| Path | What it does |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `user.name` | Access a nested property |
|
|
50
|
+
| `items[0].title` | Access an array item's property |
|
|
51
|
+
| `user.{name,email}` | Select several keys with `pick` |
|
|
52
|
+
| `user.{*}` | List keys with `pick` |
|
|
53
|
+
| `items[n].title` | Map array items with `extract` |
|
|
126
54
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Errors from getters, proxies, or deleting non-configurable properties propagate to the caller.
|
|
55
|
+
A missing path returns `undefined` with `pick`. Selection and array mapping have
|
|
56
|
+
specific uses; see the [path rules](SPEC.md#paths) for the full syntax.
|
|
130
57
|
|
|
131
|
-
##
|
|
58
|
+
## Reshape an API response
|
|
132
59
|
|
|
133
|
-
|
|
60
|
+
Describe the shape you want instead of assembling it field by field.
|
|
61
|
+
Use a list of source paths for fallbacks and `[n]` to map array items:
|
|
134
62
|
|
|
135
63
|
```ts
|
|
136
|
-
|
|
137
|
-
// true
|
|
138
|
-
|
|
139
|
-
equally({ a: 1 }, { a: 1, b: 2 })
|
|
140
|
-
// false
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`equally` treats `NaN` as equal to `NaN`, compares `Date` by timestamp and `RegExp` by
|
|
144
|
-
source and flags, and follows circular references without looping.
|
|
64
|
+
import { extract } from 'objwalk'
|
|
145
65
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
const target = { user: { name: 'Ada', flags: { admin: false } } }
|
|
66
|
+
const response = {
|
|
67
|
+
profile: { fullName: 'Natalia Romanoff' },
|
|
68
|
+
products: [{ name: 'Keyboard' }, { name: 'Mouse' }],
|
|
69
|
+
}
|
|
152
70
|
|
|
153
|
-
|
|
71
|
+
const result = extract(response, {
|
|
72
|
+
'user.name': ['profile.displayName', 'profile.fullName'],
|
|
73
|
+
'items[n].title': 'products[n].name',
|
|
74
|
+
})
|
|
154
75
|
// {
|
|
155
|
-
// user: { name: '
|
|
76
|
+
// user: { name: 'Natalia Romanoff' },
|
|
77
|
+
// items: [{ title: 'Keyboard' }, { title: 'Mouse' }],
|
|
156
78
|
// }
|
|
157
79
|
```
|
|
158
80
|
|
|
159
|
-
`
|
|
81
|
+
`extract` builds a new result object. Selected object values keep their references;
|
|
82
|
+
use `clone` when you also need to copy those values.
|
|
160
83
|
|
|
161
|
-
##
|
|
84
|
+
## Compare values and track changes
|
|
162
85
|
|
|
163
|
-
|
|
86
|
+
Compare a whole state, check only the fields that matter, or get a list of changes:
|
|
164
87
|
|
|
165
88
|
```ts
|
|
166
|
-
|
|
167
|
-
profile: {
|
|
168
|
-
fullName: 'Natalia Romanoff',
|
|
169
|
-
email: 'black.widow@example.com',
|
|
170
|
-
},
|
|
171
|
-
}
|
|
89
|
+
import { equally, diff } from 'objwalk'
|
|
172
90
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
'user.email': 'profile.email',
|
|
176
|
-
})
|
|
177
|
-
// {
|
|
178
|
-
// user: {
|
|
179
|
-
// name: 'Natalia Romanoff',
|
|
180
|
-
// email: 'black.widow@example.com',
|
|
181
|
-
// },
|
|
182
|
-
// }
|
|
183
|
-
```
|
|
91
|
+
const before = { user: { name: 'Natalia' }, hp: 10, target: 'enemy' }
|
|
92
|
+
const after = { user: { name: 'Natalia' }, hp: 8 }
|
|
184
93
|
|
|
185
|
-
|
|
94
|
+
equally(before, after) // false
|
|
95
|
+
equally(before, after, ['user.name']) // true
|
|
186
96
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
97
|
+
diff(before, after)
|
|
98
|
+
// [
|
|
99
|
+
// { type: 'set', path: ['hp'], value: 8 },
|
|
100
|
+
// { type: 'delete', path: ['target'] },
|
|
101
|
+
// ]
|
|
191
102
|
```
|
|
192
103
|
|
|
193
|
-
|
|
104
|
+
`equally` supports `NaN`, `Date`, `RegExp`, and circular references. `diff` returns
|
|
105
|
+
changes without applying them; its paths are arrays of literal keys, and changed
|
|
106
|
+
arrays are replaced as a whole. Its inputs must be acyclic.
|
|
194
107
|
|
|
195
|
-
|
|
196
|
-
const source = {
|
|
197
|
-
products: [
|
|
198
|
-
{ name: 'Keyboard' },
|
|
199
|
-
{ name: 'Mouse' },
|
|
200
|
-
],
|
|
201
|
-
}
|
|
108
|
+
## Walk objects and arrays
|
|
202
109
|
|
|
203
|
-
|
|
204
|
-
'items[n].title': 'products[n].name',
|
|
205
|
-
})
|
|
206
|
-
// {
|
|
207
|
-
// items: [
|
|
208
|
-
// { title: 'Keyboard' },
|
|
209
|
-
// { title: 'Mouse' },
|
|
210
|
-
// ],
|
|
211
|
-
// }
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## each
|
|
215
|
-
|
|
216
|
-
Walk through an object and call a function for each value.
|
|
110
|
+
`each` visits nested values and tells you where they live:
|
|
217
111
|
|
|
218
112
|
```ts
|
|
219
|
-
|
|
220
|
-
|
|
113
|
+
import { each } from 'objwalk'
|
|
114
|
+
|
|
115
|
+
each({ user: { name: 'Natalia' }, tags: ['ts'] }, (_key, value, path) => {
|
|
116
|
+
if (typeof value === 'string') {
|
|
117
|
+
console.log(path, value)
|
|
118
|
+
}
|
|
221
119
|
})
|
|
120
|
+
// user.name Natalia
|
|
121
|
+
// tags[0] ts
|
|
222
122
|
```
|
|
223
123
|
|
|
224
|
-
|
|
124
|
+
Return `false` to stop the entire walk. For more control, `each` also accepts
|
|
125
|
+
`root` and `descend` options and provides literal path segments as a fourth
|
|
126
|
+
callback argument. [Traversal details](SPEC.md#each).
|
|
225
127
|
|
|
226
|
-
|
|
227
|
-
- `value`: the current value
|
|
228
|
-
- `path`: the full path to the current value
|
|
229
|
-
|
|
230
|
-
## map
|
|
231
|
-
|
|
232
|
-
Walk through an object and build a new result.
|
|
128
|
+
Use `map` when the walk should build a result:
|
|
233
129
|
|
|
234
130
|
```ts
|
|
235
|
-
|
|
131
|
+
import { map, set } from 'objwalk'
|
|
132
|
+
|
|
133
|
+
const cleaned = map({ name: ' Natalia ', tags: [' ts '] }, (_key, value, store, path) => {
|
|
236
134
|
if (typeof value === 'string') {
|
|
237
135
|
set(store, path, value.trim())
|
|
238
136
|
}
|
|
239
137
|
})
|
|
138
|
+
// { name: 'Natalia', tags: ['ts'] }
|
|
240
139
|
```
|
|
241
140
|
|
|
242
|
-
|
|
141
|
+
`map` returns its store and ignores callback return values. Both walkers expect
|
|
142
|
+
acyclic data.
|
|
243
143
|
|
|
244
|
-
|
|
245
|
-
- `value`: the current value
|
|
246
|
-
- `store`: the result object being built
|
|
247
|
-
- `path`: the full path to the current value
|
|
144
|
+
## Merge defaults or make a copy
|
|
248
145
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
## parse and stringify
|
|
252
|
-
|
|
253
|
-
Parse JSON or accept a ready value, and serialize values with optional error handling.
|
|
254
|
-
Exceptions return `undefined`, unless a fallback is provided or `onError` throws.
|
|
146
|
+
Combine nested settings while keeping the original defaults:
|
|
255
147
|
|
|
256
148
|
```ts
|
|
257
|
-
import {
|
|
258
|
-
|
|
259
|
-
// Signatures:
|
|
260
|
-
// parse(raw: unknown, options?: ParseOptions): unknown
|
|
261
|
-
// stringify(value: unknown, options?: StringifyOptions): string | undefined
|
|
262
|
-
|
|
263
|
-
const value = parse('{"a":1}') // { a: 1 }, typed as unknown
|
|
264
|
-
const valueOrNull = parse('{', { fallback: null }) // null
|
|
265
|
-
const text = stringify(value, { fallback: 'null', space: 2 })
|
|
266
|
-
const object = { a: 1 }
|
|
267
|
-
const array = [1, 2]
|
|
268
|
-
|
|
269
|
-
parse(object) === object // true
|
|
270
|
-
parse(array) === array // true
|
|
271
|
-
|
|
272
|
-
parse('[]') // []
|
|
273
|
-
stringify({}) // '{}'
|
|
274
|
-
stringify([]) // '[]'
|
|
275
|
-
stringify(null) // 'null'
|
|
276
|
-
stringify('{}') // '"{}"'
|
|
277
|
-
```
|
|
149
|
+
import { clone, merge } from 'objwalk'
|
|
278
150
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
Other inputs use `JSON.parse(typeof raw === 'string' ? raw : String(raw), reviver)`.
|
|
287
|
-
ArrayBuffer, DataView, and typed-array views use this text path; there is no binary decoder.
|
|
288
|
-
For example, a Node.js Buffer containing JSON text can be parsed through its native string conversion.
|
|
289
|
-
Errors during input recognition or string conversion follow the same `onError`/`fallback` contract.
|
|
290
|
-
|
|
291
|
-
| Option | `ParseOptions` | `StringifyOptions` |
|
|
292
|
-
| --- | --- | --- |
|
|
293
|
-
| `fallback` | `unknown`, default `undefined` | `string`, default `undefined` |
|
|
294
|
-
| `onError` | `(error: unknown) => void` | `(error: unknown) => void` |
|
|
295
|
-
| `reviver` | Native `JSON.parse` callback, only for the text path | — |
|
|
296
|
-
| `replacer` | — | Native callback or array of strings/numbers (also accepts `null`) |
|
|
297
|
-
| `space` | — | `string` or `number` for native JSON formatting |
|
|
298
|
-
|
|
299
|
-
Callbacks retain native `this` binding. For example, a reviver can transform or delete properties,
|
|
300
|
-
and a replacer can select or transform values:
|
|
151
|
+
const defaults = { theme: { mode: 'light', contrast: 1 }, tags: ['default'] }
|
|
152
|
+
const settings = merge(clone(defaults), {
|
|
153
|
+
theme: { mode: 'dark' },
|
|
154
|
+
tags: ['custom'],
|
|
155
|
+
})
|
|
156
|
+
// { theme: { mode: 'dark', contrast: 1 }, tags: ['custom'] }
|
|
157
|
+
```
|
|
301
158
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}) // { count: 2 }
|
|
159
|
+
`merge` combines plain objects and replaces arrays. `clone` copies nested objects
|
|
160
|
+
and arrays, including circular references, with support for `Date` and `RegExp`.
|
|
161
|
+
See the [copying contract](SPEC.md#clone) for supported types.
|
|
306
162
|
|
|
307
|
-
|
|
308
|
-
// '{\n "name": "Ada"\n}'
|
|
309
|
-
```
|
|
163
|
+
## Handle JSON at the boundary
|
|
310
164
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
follow the same rule. If `onError` throws, its exception propagates to the caller:
|
|
165
|
+
`parse` accepts JSON text or a ready object or array. `stringify` serializes a value.
|
|
166
|
+
Use a fallback when an operation fails:
|
|
314
167
|
|
|
315
168
|
```ts
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}) //
|
|
321
|
-
|
|
322
|
-
stringify(1n, { fallback: 'null' }) // 'null' (BigInt throws natively)
|
|
323
|
-
stringify(undefined, { fallback: 'null' }) // undefined (no exception)
|
|
324
|
-
parse('false', { fallback: true }) // false
|
|
325
|
-
parse('null', { fallback: {} }) // null
|
|
169
|
+
import { parse, stringify } from 'objwalk'
|
|
170
|
+
|
|
171
|
+
parse('{"enabled":true}') // { enabled: true }
|
|
172
|
+
parse({ enabled: true }) // { enabled: true }, same object
|
|
173
|
+
parse('invalid JSON', { fallback: null }) // null
|
|
174
|
+
stringify({ enabled: true }) // '{"enabled":true}'
|
|
326
175
|
```
|
|
327
176
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
is preserved. Circular references throw natively and use the same error handling as BigInt.
|
|
333
|
-
The wrappers do not mutate inputs, undo callback side effects, log errors, or repair invalid JSON.
|
|
177
|
+
Without a fallback, errors return `undefined`. Native `reviver`, `replacer`,
|
|
178
|
+
formatting, and an `onError` callback are available through options.
|
|
179
|
+
`parse` returns `unknown`; validate the result before using it as a domain type.
|
|
180
|
+
[JSON options and error handling](SPEC.md#parse-and-stringify).
|
|
334
181
|
|
|
335
|
-
##
|
|
182
|
+
## API
|
|
336
183
|
|
|
337
|
-
|
|
|
184
|
+
| Task | Function |
|
|
338
185
|
| --- | --- |
|
|
339
|
-
|
|
|
340
|
-
|
|
|
341
|
-
|
|
|
342
|
-
|
|
|
343
|
-
|
|
|
186
|
+
| Read a value or select keys | [`pick(data, path?, options?)`](SPEC.md#pick) |
|
|
187
|
+
| Write a value by path | [`set(data, path, value)`](SPEC.md#set) |
|
|
188
|
+
| Delete one or several paths | [`flush(data, path)`](SPEC.md#flush) |
|
|
189
|
+
| Reshape data with a mapping | [`extract(source, mapping)`](SPEC.md#extract) |
|
|
190
|
+
| Visit nested values | [`each(data, callback, options?)`](SPEC.md#each) |
|
|
191
|
+
| Build a result while walking | [`map(data, callback, store?)`](SPEC.md#map) |
|
|
192
|
+
| Merge plain objects | [`merge(target, ...sources)`](SPEC.md#merge) |
|
|
193
|
+
| Copy supported values | [`clone(value)`](SPEC.md#clone) |
|
|
194
|
+
| Compare values or selected paths | [`equally(a, b, paths?)`](SPEC.md#equally) |
|
|
195
|
+
| Describe changes between values | [`diff(first, last)`](SPEC.md#diff) |
|
|
196
|
+
| Parse JSON or accept a ready value | [`parse(raw, options?)`](SPEC.md#parse-and-stringify) |
|
|
197
|
+
| Serialize a value as JSON | [`stringify(value, options?)`](SPEC.md#parse-and-stringify) |
|
|
198
|
+
|
|
199
|
+
For exact types, mutation rules, and edge cases, use the [full specification](SPEC.md).
|
|
344
200
|
|
|
345
201
|
## Agent skill
|
|
346
202
|
|
|
347
|
-
The package includes an [objwalk skill](skills/objwalk/SKILL.md)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
The skill explains the existing object utilities and JSON helpers, and directs agents to use
|
|
352
|
-
them without redundant wrappers that only repeat object checks.
|
|
353
|
-
|
|
354
|
-
## API
|
|
355
|
-
|
|
356
|
-
```ts
|
|
357
|
-
pick(data, path)
|
|
358
|
-
set(data, path, value)
|
|
359
|
-
flush(data, path)
|
|
360
|
-
extract(data, mapping)
|
|
361
|
-
each(data, callback)
|
|
362
|
-
map(data, callback)
|
|
363
|
-
merge(target, ...sources)
|
|
364
|
-
equally(a, b)
|
|
365
|
-
parse(raw, options?) // unknown
|
|
366
|
-
stringify(value, options?) // string | undefined
|
|
367
|
-
```
|
|
203
|
+
The package includes an [objwalk skill](skills/objwalk/SKILL.md). Point your agent at
|
|
204
|
+
`node_modules/objwalk/skills/objwalk/SKILL.md`, or link the entire `skills/objwalk`
|
|
205
|
+
folder into its skill directory.
|