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 CHANGED
@@ -1,10 +1,15 @@
1
1
  # objwalk
2
2
 
3
- Small utilities for reading, writing, walking, and reshaping nested JavaScript objects.
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
- `objwalk` is built around a compact path syntax, so the same mental model works for picking, setting, and deleting values, extracting a new object shape, and walking through nested data.
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
- ```ts
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
- const data = {
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
- items: [{ title: 'First' }, { title: 'Second' }],
28
+ user: { name: 'Natalia Romanoff' },
29
+ items: [{ title: 'Keyboard', internalId: 'draft-1' }],
56
30
  }
57
31
 
58
- pick(data, 'items[1].title')
59
- // 'Second'
60
- ```
61
-
62
- If the path cannot be resolved, `pick` returns `undefined`.
63
-
64
- By default, `pick` keeps its permissive property lookup behavior. Pass options to restrict
65
- lookups to own properties or ignore specific keys.
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(data, path)` mutates and returns the same object. Missing paths, `null`,
114
- `undefined`, and primitive parents are left unchanged. It does not create intermediate
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
- Array indexes use the same explicit syntax as `pick` and `set`:
45
+ ## Paths at a glance
119
46
 
120
- ```ts
121
- const data = { items: [{ title: 'First' }, { title: 'Second' }] }
122
-
123
- flush(data, 'items[0].title') // { items: [{}, { title: 'Second' }] }
124
- flush(data, 'items[1]') // { items: [{}, <empty item>] }
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
- Deleting an array item leaves a hole, preserves its length, and does not shift later items.
128
- Selection syntax (`{name,email}`, `{*}`) and `[n]` mappings are not supported by `flush`.
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
- ## equally
58
+ ## Reshape an API response
132
59
 
133
- Deeply compare two values for structural equality.
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
- equally({ a: { b: [1, 2] } }, { a: { b: [1, 2] } })
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
- ## merge
147
-
148
- Deeply merge plain objects into a target object.
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
- merge(target, { user: { email: 'ada@example.com', flags: { admin: true } } })
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: 'Ada', email: 'ada@example.com', flags: { admin: true } },
76
+ // user: { name: 'Natalia Romanoff' },
77
+ // items: [{ title: 'Keyboard' }, { title: 'Mouse' }],
156
78
  // }
157
79
  ```
158
80
 
159
- `merge` mutates and returns `target`. Sources are applied from left to right. Arrays and non-plain objects, such as `Date`, replace the existing value rather than being merged.
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
- ## extract
84
+ ## Compare values and track changes
162
85
 
163
- Create a new object by mapping destination paths to source paths.
86
+ Compare a whole state, check only the fields that matter, or get a list of changes:
164
87
 
165
88
  ```ts
166
- const source = {
167
- profile: {
168
- fullName: 'Natalia Romanoff',
169
- email: 'black.widow@example.com',
170
- },
171
- }
89
+ import { equally, diff } from 'objwalk'
172
90
 
173
- extract(source, {
174
- 'user.name': 'profile.fullName',
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
- Use an array of source paths to provide fallbacks. The first resolved value is used.
94
+ equally(before, after) // false
95
+ equally(before, after, ['user.name']) // true
186
96
 
187
- ```ts
188
- extract(source, {
189
- 'user.name': ['profile.displayName', 'profile.fullName'],
190
- })
97
+ diff(before, after)
98
+ // [
99
+ // { type: 'set', path: ['hp'], value: 8 },
100
+ // { type: 'delete', path: ['target'] },
101
+ // ]
191
102
  ```
192
103
 
193
- Use `[n]` to map over arrays.
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
- ```ts
196
- const source = {
197
- products: [
198
- { name: 'Keyboard' },
199
- { name: 'Mouse' },
200
- ],
201
- }
108
+ ## Walk objects and arrays
202
109
 
203
- extract(source, {
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
- each(data, (key, value, path) => {
220
- console.log(key, value, path)
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
- The callback receives:
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
- - `key`: the current object key or array index
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
- const result = map(data, (key, value, store, path) => {
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
- The callback receives:
141
+ `map` returns its store and ignores callback return values. Both walkers expect
142
+ acyclic data.
243
143
 
244
- - `key`: the current object key or array index
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
- `map` returns `store`.
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 { parse, stringify, type ParseOptions, type StringifyOptions } from 'objwalk'
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
- Root values can be objects, arrays, strings, numbers, booleans, or `null`;
280
- neither function requires an expected shape or restricts values to objects.
281
- `parse` returns `unknown`; successful parsing does not validate the expected data structure.
282
- Validate the result before using it as a specific type. There is no `parse<T>()` overload.
283
-
284
- `parse` accepts `unknown`. Plain objects (with `Object.prototype` or a `null` prototype) and
285
- arrays are returned by reference, without cloning, serialization, mutation, or calling `reviver`.
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
- ```ts
303
- parse('{"count":2,"secret":true}', {
304
- reviver: (key, value) => key === 'secret' ? undefined : value,
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
- stringify({ name: 'Ada', secret: true }, { replacer: ['name'], space: 2 })
308
- // '{\n "name": "Ada"\n}'
309
- ```
163
+ ## Handle JSON at the boundary
310
164
 
311
- On an exception, `onError` receives the original error once, then the function returns `fallback`.
312
- The callback's return value is ignored. Exceptions from `reviver`, `replacer`, `toJSON`, or getters
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
- const requiredText = stringify(value, {
317
- onError: (error) => {
318
- throw error
319
- },
320
- }) // still string | undefined
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
- Fallbacks apply only to exceptions: valid `0`, `false`, `''`, and `null` are preserved.
329
- Likewise, a reviver returning `undefined` for the root does not trigger a fallback.
330
- Top-level `undefined`, functions, and symbols serialize to native `undefined` without calling
331
- `onError`. Native handling of these values inside objects/arrays, `NaN`, `Infinity`, and `toJSON`
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
- ## Path syntax
182
+ ## API
336
183
 
337
- | Syntax | Description |
184
+ | Task | Function |
338
185
  | --- | --- |
339
- | `user.name` | Read, write, or delete a nested property. |
340
- | `items[0].title` | Read, write, or delete an array item's property by index. |
341
- | `user.{name,email}` | Pick several keys from one object. |
342
- | `user.{*}` | List keys from one object. |
343
- | `items[n].title` | Map every item in an array during `extract`. |
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) in `skills/objwalk/`.
348
- After installation, point your agent at `node_modules/objwalk/skills/objwalk/SKILL.md`.
349
- If your agent uses a skill directory, link the entire `skills/objwalk` folder, not just `SKILL.md`.
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.