objwalk 1.6.0 → 1.8.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/SPEC.md ADDED
@@ -0,0 +1,442 @@
1
+ # objwalk specification
2
+
3
+ This document describes the public API in the current source tree, including changes
4
+ not yet released. [README.md](README.md) provides examples; [tests](tests/) exercise
5
+ the contracts.
6
+
7
+ ## Public API and shared types
8
+
9
+ All functions and types below are named exports from `objwalk`.
10
+
11
+ ```ts
12
+ type ObjwalkObject = Record<string, any>
13
+ type WalkKey = string | number
14
+ type ExtractMapping = Record<string, string | string[]>
15
+
16
+ type PickOptions = {
17
+ inherited?: boolean
18
+ ignore?: readonly string[]
19
+ }
20
+
21
+ type DiffOperation =
22
+ | { type: 'set'; path: string[]; value: unknown }
23
+ | { type: 'delete'; path: string[] }
24
+ ```
25
+
26
+ | Function | Return value | Mutation and identity |
27
+ | --- | --- | --- |
28
+ | `pick(data, path?, options?)` | Selected value, selection object, key list, or `undefined` | Selected values retain their references |
29
+ | `set(data, path, value)` | Input object | Mutates the input; assigned values are not cloned |
30
+ | `flush(data, path)` | Input object | Mutates the input |
31
+ | `extract(source, mapping)` | New result object | Selected values are not cloned |
32
+ | `each(data, callback, options?)` | `undefined` | Passes original values to callbacks |
33
+ | `map(data, callback, options?)` | New array or dictionary | Callback returns each replacement value; pruned and returned values can retain references |
34
+ | `merge(target, ...sources)` | Target object | Mutates the target; replacement values are not cloned |
35
+ | `clone(value)` | Value of the same declared generic type | Recursively copies supported objects and arrays |
36
+ | `equally(a, b, paths?)` | `boolean` | Does not assign to the inputs |
37
+ | `diff(first, last)` | `DiffOperation[]` | Does not assign to inputs; operation values retain references |
38
+ | `parse(raw, options?)` | `unknown` | Ready objects and arrays retain their references |
39
+ | `stringify(value, options?)` | `string \| undefined` | Uses native JSON serialization |
40
+
41
+ `pick`, `set`, and `flush` use `{}` when their `data` argument is `undefined`.
42
+ `set` and `flush` still require the path argument. `pick` defaults its path to `''`.
43
+ Their declared data contract is `ObjwalkObject`; this is not a guarantee that all
44
+ functions accept every possible JavaScript value. `each` accepts `any`; `map`
45
+ accepts dictionaries and readonly arrays; `clone` is generic; `equally`, `diff`, `parse`, and `stringify` accept `unknown` inputs.
46
+ `extract` requires an object and a mapping; `merge` requires an object target.
47
+
48
+ Except for the documented JSON error handling, exceptions from callbacks, getters,
49
+ setters, proxies, or native property operations propagate. Mutations already made
50
+ before an exception are not rolled back. Functions are synchronous and do not await
51
+ callback results. User callbacks and property accessors can have their own side effects.
52
+
53
+ ## Paths
54
+
55
+ Paths are strings, not arrays of literal segments. A dot separates object properties.
56
+ An explicit array segment has the form `items[0]`; its parser matches
57
+ `^(\w+)\[(\d+)\]$`. Use canonical non-negative decimal indexes and a word-character
58
+ property name for this syntax. A bracket segment resolves only when its named value
59
+ is an array. Ordinary segments use property lookup.
60
+
61
+ Supported examples are `user.name`, `items[0].title`, and
62
+ `groups[0].items[1].id`. Quoted brackets, escaping dots, negative indexes, and nested
63
+ brackets such as `matrix[0][1]` are not supported path operators. Unsupported syntax
64
+ is not automatically rejected: a segment can instead be treated as a literal key.
65
+ There is no general path validator.
66
+
67
+ | Syntax | Interpretation |
68
+ | --- | --- |
69
+ | `user.name` | Property traversal |
70
+ | `items[0].title` | Explicit array index followed by property traversal |
71
+ | `user.{name,email}` | Terminal key selection in `pick` |
72
+ | `user.{*}` | Terminal key listing in `pick` |
73
+ | `items[n].title` | Array mapping in `extract`, paired with a source mapping |
74
+
75
+ Key selection is interpreted by `pick`, including source paths passed through
76
+ `pick` by `extract`. It is not interpreted by `set`, `flush`, or `equally`.
77
+ `[n]` is not a wildcard for the other functions.
78
+
79
+ Empty paths have operation-specific meanings:
80
+
81
+ - `pick(data, '')` returns the root value.
82
+ - `equally(a, b, [''])` compares the whole input values.
83
+ - `flush(data, '')` does nothing.
84
+ - `set(data, '', value)` assigns the property whose key is the empty string.
85
+
86
+ Traversal paths from `each` and `map` are descriptive strings, not a lossless encoding
87
+ of arbitrary keys. Literal dots, brackets, and empty keys can make them ambiguous.
88
+ A root array produces paths such as `[0]`, which do not have the named-array-segment
89
+ form above. Use `each`'s literal `segments` or `diff`'s operation paths when exact keys matter.
90
+
91
+ ## pick
92
+
93
+ `pick(data, path?, options?)` reads a path without creating intermediate containers.
94
+ Missing or nullish intermediate values produce `undefined`. Valid falsy values are
95
+ preserved. An explicit array index outside the array resolves to `undefined`, unless
96
+ a value is available through the configured property lookup.
97
+
98
+ `PickOptions` defaults to `{ inherited: true, ignore: [] }`. `inherited: false`
99
+ restricts every property and array-index lookup to own properties. `ignore` matches
100
+ exact string keys, including array indexes represented as strings, at every lookup.
101
+
102
+ Terminal selection trims whitespace around comma-separated keys and removes empty
103
+ entries. One selected key returns its value. Multiple selected keys return a new
104
+ object containing every selected key, including keys whose values are `undefined`.
105
+ Selection requires an object value; an unresolved or primitive selection base returns
106
+ `undefined`. The selection operation does not clone selected values.
107
+
108
+ `{*}` returns own enumerable string keys, filtered by `ignore`, in `Object.keys`
109
+ order. It returns `[]` for arrays, primitives, nullish values, or unresolved paths;
110
+ it does not list inherited keys even when inherited lookup is enabled.
111
+
112
+ ## set
113
+
114
+ `set(data, path, value)` mutates and returns the same input object. It uses direct
115
+ property lookup and assignment, without `PickOptions` or the key filter from `merge`.
116
+
117
+ For an ordinary intermediate segment, missing, nullish, and primitive values are
118
+ replaced with `{}`. Existing object values, including arrays, are reused.
119
+ For an array segment, a non-array named value is replaced with `[]`. A later index
120
+ extends the array, leaving holes before that index. An intermediate indexed value
121
+ is initialized to `{}` only when it is `undefined`; existing null or primitive
122
+ indexed values are not normalized into objects for further traversal.
123
+
124
+ The final property or array slot is assigned even when `value` is `undefined`.
125
+ Assignment is distinct from deletion. No empty-parent pruning or rollback occurs.
126
+
127
+ ## flush
128
+
129
+ `flush(data, path)` accepts `path: string | string[]`. Each path deletes an own
130
+ property using native `delete`, and the function returns the same input object.
131
+
132
+ - Path arrays are processed in order and are not modified. Holes are skipped.
133
+ - Empty lists, empty paths, missing paths, and nullish or primitive parents are no-ops.
134
+ - Traversal and deletion use only own properties, including explicit array indexes.
135
+ - Missing containers are not created; empty parents are not removed.
136
+ - Overlapping and repeated paths are processed against the state left by earlier paths.
137
+ - Deleting an array slot leaves a hole; length and later indexes are unchanged.
138
+ - An exception stops processing immediately. Earlier deletions remain applied and
139
+ later paths are not processed.
140
+
141
+ ## extract
142
+
143
+ `extract(source, mapping)` creates a result root. Mapping keys are destination paths;
144
+ values are source paths or ordered arrays of fallback source paths.
145
+
146
+ Mappings are processed in `Object.entries(mapping)` order. A direct source path is
147
+ read with `pick` and written with `set`, even when its value is `undefined`.
148
+ A fallback array selects the first value that is not `undefined`; `null`, `false`,
149
+ `0`, and `''` all count as resolved. Holes are skipped and later paths are not read
150
+ after a match. If none resolve, that mapping performs no assignment.
151
+
152
+ For array mapping, use one `[n].` marker in both the destination path and its string
153
+ source path, for example `'items[n].title': 'products[n].name'`. The source prefix
154
+ must resolve to an array. Each present source item is read using the source suffix
155
+ and assigned at the corresponding destination index. A non-array source produces
156
+ no assignments. Multiple mapping markers and fallback arrays of array mappings
157
+ are outside this mapping contract.
158
+
159
+ The result root is new, but selected objects are assigned by reference. Overlapping
160
+ destination writes can therefore modify objects shared with the source. `extract`
161
+ does not provide an independent deep snapshot.
162
+
163
+ ## each
164
+
165
+ ```ts
166
+ type EachCallback<TKey extends WalkKey | undefined = WalkKey> = (
167
+ key: TKey,
168
+ value: any,
169
+ path: string,
170
+ segments: WalkKey[]
171
+ ) => unknown
172
+
173
+ type EachEntry = {
174
+ key: WalkKey | undefined
175
+ value: any
176
+ path: string
177
+ segments: WalkKey[]
178
+ }
179
+
180
+ type EachOptions = {
181
+ root?: boolean
182
+ descend?: (entry: EachEntry) => boolean
183
+ }
184
+ ```
185
+
186
+ `each(data, callback, options?)` walks objects and arrays depth first, visiting a
187
+ value before its children. Objects enumerate own enumerable string keys in
188
+ `Object.keys` order; symbols and non-enumerable properties are not enumerated.
189
+ Arrays visit numeric indexes in ascending order, using native `every` iteration:
190
+ absent slots are skipped, inherited indexed values can be visited, and non-index
191
+ properties are not traversed. Any non-null object can be a container; functions
192
+ are leaves, and built-in collection internals are not traversed.
193
+
194
+ `root` defaults to `false`. When enabled, the root is visited first with key
195
+ `undefined`, path `''`, and segments `[]`, even for primitive, nullish, or empty
196
+ inputs. Otherwise a primitive or nullish root has no visits. Object keys are
197
+ strings and array indexes are numbers. Without root visits, callback keys are
198
+ typed as `WalkKey`; enabled or dynamic root options use `WalkKey | undefined`.
199
+
200
+ Returning exactly `false` from the callback stops the entire traversal, including
201
+ siblings and remaining ancestor branches. Other return values, including other
202
+ falsy values, are ignored. A stopped container does not invoke `descend` or visit
203
+ children; subsequent values are not read. The function returns `undefined` after
204
+ both completion and early stopping.
205
+
206
+ `descend` is evaluated only for containers, after their callback. It also runs for
207
+ an excluded root container. Returning `false` skips that container's children and
208
+ continues with siblings. It does not stop the entire traversal.
209
+
210
+ Object keys are collected when entering a container; values are read as visited.
211
+ For arrays, native iteration captures the starting length: appended slots beyond
212
+ that length are not visited. Callback and `descend` receive separate copies of
213
+ `segments`; modifying those copies does not change traversal paths. `value` remains
214
+ the original value. Cycles are not detected. Separate and nested invocations do
215
+ not share stop state.
216
+
217
+ ## map
218
+
219
+ ```ts
220
+ type MapCallback<TResult = unknown, TKey extends WalkKey = WalkKey> = (
221
+ key: TKey,
222
+ value: any,
223
+ path: string
224
+ ) => TResult
225
+
226
+ type MapOptions = {
227
+ descend?: EachOptions['descend']
228
+ shallow?: boolean
229
+ }
230
+ ```
231
+
232
+ `map(data, callback, options?)` transforms a dictionary or array without assigning
233
+ into the input. An array root produces a new array; a dictionary root produces a
234
+ new ordinary object. The root is excluded from callback visits. Every visited
235
+ property or array index retains its literal key; its value becomes the callback's
236
+ return value. `false`, `null`, and `undefined` are stored as ordinary results;
237
+ there is no callback return value for stopping, filtering, or deleting entries.
238
+ Callback replacements are not traversed again, including returned objects and arrays.
239
+
240
+ The callback receives `(key, value, path)`. Object keys are strings, array indexes
241
+ are numbers, and paths follow `each`'s descriptive string format. No accumulator or
242
+ store argument exists. `MapCallback`'s first generic argument is the return value
243
+ rather than the former store type; the optional second argument is the key type.
244
+ Array results are typed as `TResult[]`; dictionary results as
245
+ `Record<string, TResult>`. A dictionary-or-array union input produces the matching
246
+ union result type. Readonly input arrays are accepted and produce mutable new arrays.
247
+ Deep callback values are typed as `any`, since earlier child transformations can
248
+ change their contents.
249
+
250
+ Deep traversal is the default. Plain objects and arrays are recursively mapped
251
+ before their containing value is passed to the callback. Thus callbacks run in
252
+ postorder: children first, then their parent, with the root still excluded.
253
+ A container callback receives a newly built container containing its already
254
+ mapped children. A pruned container callback instead receives the original value
255
+ by reference. `Date`, `RegExp`, other non-plain objects, and functions are leaves:
256
+ they are passed directly to the callback without rebuilding their internal state.
257
+ The root dictionary's own entries are enumerated regardless of its prototype;
258
+ the plain-object restriction applies to nested traversal.
259
+
260
+ Objects enumerate own enumerable string keys; inherited, symbol, and non-enumerable
261
+ properties are excluded. Literal dots, brackets, empty keys, and `__proto__` are
262
+ preserved as own data properties rather than parsed as paths. Result objects use
263
+ `Object.prototype`; source prototypes and descriptors are not copied. Array indexes
264
+ follow `each` iteration, preserving length and absent slots. An explicitly returned
265
+ `undefined` is an own slot, whereas an unvisited hole remains absent. Inherited
266
+ indexed values can be visited; extra array properties are excluded.
267
+
268
+ `descend` runs on each original traversable container before visiting children,
269
+ including the excluded root. It receives `EachEntry` with literal `segments`;
270
+ modifying these segments does not alter traversal paths. Returning `false` prunes
271
+ children, then the container's callback processes the original value as a whole.
272
+ Pruning the root produces an empty container of the same kind, with no callback
273
+ visits. This timing differs from `each`, which calls its callback before `descend`.
274
+
275
+ `shallow` defaults to `false`. With literal `shallow: true`, only immediate root
276
+ children are mapped: dictionary callback keys are typed as `string`, and statically
277
+ known array keys as `number`. Deep calls, dynamic shallow flags, and union inputs
278
+ keep `WalkKey`. An arbitrary `descend` predicate does not narrow keys. When both
279
+ options are present, `descend` is evaluated only for the root, because child
280
+ containers are already at the shallow limit.
281
+
282
+ Traversed containers are new. Pruned containers, special objects, and values
283
+ returned by the callback may share references with the input. Each occurrence of
284
+ a shared traversable container is mapped separately. Cycles are not detected;
285
+ pruning can prevent descent into a cyclic branch. Exceptions from callbacks,
286
+ getters, and `descend` propagate. The function does not roll back callback side
287
+ effects or await promises returned by callbacks.
288
+
289
+ This replaces the published accumulator-based contract. Migrate
290
+ `map(data, (key, value, store, path) => { store[key] = transform(value) }, store)`
291
+ to `map(data, (key, value, path) => transform(value), options)` when the result
292
+ retains input keys. Callers that build a different structure should use `each`
293
+ and manage their accumulator explicitly. Old stores are not accepted as options,
294
+ and there is no compatibility overload or `undefined` placeholder.
295
+
296
+ ## merge
297
+
298
+ `merge<T extends ObjwalkObject>(target: T, ...sources: unknown[]): T` mutates and
299
+ returns the target. Sources are applied left to right. A source is processed only
300
+ when its prototype is `Object.prototype` or `null`; other root sources are ignored.
301
+
302
+ Each source uses its own enumerable string entries. Entry values are read by
303
+ `Object.entries` before that source's assignments begin. Keys `__proto__`,
304
+ `constructor`, and `prototype` are skipped at every recursive merge level.
305
+ Symbols and non-enumerable source keys are not merged.
306
+
307
+ When the current target value and source value are both plain objects, they are
308
+ merged recursively. Otherwise the source value replaces the target value by
309
+ reference, including arrays, `Date`, `null`, and `undefined`. Replacement objects
310
+ are not traversed just because they have been assigned. The operation does not
311
+ guarantee cycle handling or preserve a snapshot of source values beyond each
312
+ source's entry collection.
313
+
314
+ ## clone
315
+
316
+ `clone<T>(value: T): T` returns primitive values and functions unchanged. Arrays
317
+ and ordinary object properties are recursively copied with a reference store,
318
+ preserving their shared references and circular links within the copied graph.
319
+
320
+ - Arrays copy indexed values up to their length into a new array. Holes become
321
+ explicit `undefined` entries; extra properties and symbol keys are not copied.
322
+ - Other objects use `Reflect.ownKeys`, including symbols and non-enumerable keys,
323
+ to read values and assign them to a new ordinary object. Custom prototypes and
324
+ property descriptors are not preserved; accessors are evaluated rather than copied.
325
+ - `Date` uses a new instance with the same timestamp; `RegExp` uses its source and
326
+ flags. Their custom properties are not copied, `lastIndex` is not retained, and
327
+ repeated references to these special values are not deduplicated by the store.
328
+ - There is no specialized copying of `Map`, `Set`, typed-array internal state, or
329
+ class invariants. The generic return type does not guarantee preservation of
330
+ those runtime capabilities.
331
+
332
+ Property access errors propagate. The implementation does not assign to the source.
333
+
334
+ ## equally
335
+
336
+ `equally(a: unknown, b: unknown, paths?: string[]): boolean` performs deep comparison.
337
+ Omitting `paths`, or passing `undefined`, compares the whole values. Providing a
338
+ path list compares only the selected values; all must match. An empty list returns
339
+ `true`, holes are skipped, and comparison stops at the first mismatch. Unselected
340
+ properties and later paths are not read.
341
+
342
+ Path reads use ordinary dot and explicit array-index lookup with inherited
343
+ properties enabled. Missing paths resolve to `undefined`, so a missing value and
344
+ an explicit `undefined` value compare equally. `''` selects the root. Selection
345
+ syntax and `[n]` mappings are not interpreted.
346
+
347
+ For whole values or each selected value:
348
+
349
+ - Strictly identical values compare equal, including `0` and `-0`.
350
+ - `NaN` compares equal to `NaN`. Other primitive values use strict equality.
351
+ - Two `Date` instances compare timestamps; two `RegExp` instances compare source
352
+ and flags. Distinct invalid dates have unequal timestamps under strict equality.
353
+ A `Date` or `RegExp` cannot equal a value of the other type or an ordinary object.
354
+ - An array and a non-array compare unequal. Arrays compare lengths and indexed
355
+ values in both dense and sparse arrays. A hole reads as `undefined`, including
356
+ when compared against an explicit `undefined` slot. Inherited indexed values
357
+ follow ordinary property lookup. Comparison is independent of argument order
358
+ for inputs without access side effects. Values are read once per compared index,
359
+ and reading stops at the first mismatch. Extra string and symbol properties
360
+ are not compared.
361
+ - Other objects compare all own string and symbol keys, including non-enumerable
362
+ keys, then recursively compare values. Key order, prototypes, and descriptors
363
+ are not compared. Accessors are evaluated.
364
+ - Previously visited object pairs allow circular graphs to terminate; alias
365
+ identity across otherwise equal object graphs is not required.
366
+
367
+ There is no specialized comparison of collection internal state such as `Map`
368
+ or `Set` entries. The same rules apply to whole-value and selected-path comparisons.
369
+
370
+ ## diff
371
+
372
+ `diff(first: unknown, last: unknown): DiffOperation[]` describes changes from the
373
+ first value to the last. Equal inputs produce `[]`.
374
+
375
+ When both values at a location are plain objects, comparison recurses through own
376
+ enumerable string keys. Inherited, symbol, and non-enumerable properties are not
377
+ part of that recursion. Other values, including arrays, use `equally` and are
378
+ replaced as a whole when unequal. Newly added values are also set as a whole.
379
+
380
+ Paths are arrays of literal string keys. `[]` identifies a root replacement.
381
+ They are not string-path arguments for `pick`, `set`, or `flush`, and must not be
382
+ joined with dots when keys can contain path punctuation.
383
+
384
+ Changed and added values are visited in the last object's key order. Removals
385
+ are emitted when leaving their containing branch, in the first object's key
386
+ order, before moving to the next branch. An own `undefined` value is distinct
387
+ from deletion. Empty parents remain; prototype and descriptor changes are not
388
+ reported during plain-object recursion.
389
+
390
+ Operations contain original references from `last`, without cloning. No patch
391
+ applier, options argument, or cycle detection is provided. Acyclic shared references
392
+ are supported. JSON serialization can drop an `undefined` operation value, so the
393
+ operation list is not a lossless JSON transport for every JavaScript value.
394
+
395
+ ## parse and stringify
396
+
397
+ ```ts
398
+ type ParseOptions = {
399
+ fallback?: unknown
400
+ onError?: (error: unknown) => void
401
+ reviver?: Parameters<typeof JSON.parse>[1]
402
+ }
403
+
404
+ type StringifyOptions = {
405
+ fallback?: string
406
+ onError?: (error: unknown) => void
407
+ replacer?: ParseOptions['reviver'] | Parameters<typeof JSON.stringify>[1]
408
+ space?: string | number
409
+ }
410
+ ```
411
+
412
+ `parse(raw: unknown, options?): unknown` returns ready arrays and plain objects
413
+ by reference without walking them or calling `reviver`. Plain objects include
414
+ null-prototype objects. Other values use
415
+ `JSON.parse(typeof raw === 'string' ? raw : String(raw), reviver)`.
416
+ `ArrayBuffer` and array-buffer views always use the text path; there is no binary
417
+ decoder. The result can be any JSON root type, or the arbitrary result of a
418
+ reviver. There is no generic `parse<T>()` or object-only mode.
419
+
420
+ `stringify(value: unknown, options?): string | undefined` always calls native
421
+ `JSON.stringify` with `replacer` and `space`. Strings are serialized as strings,
422
+ not treated as already serialized JSON. Native omission, array-null replacement,
423
+ formatting, `toJSON`, and callback `this` binding are preserved.
424
+
425
+ Both functions catch exceptions from their normal operation, including recognition,
426
+ conversion, getters, proxies, and JSON callbacks. On an exception, `onError`, when
427
+ provided, receives the original thrown value once, then the function returns
428
+ `fallback`, which defaults to `undefined`. A throwing `onError` propagates its
429
+ exception without being called again. Its return value is ignored.
430
+
431
+ Fallback applies only to exceptions. Valid `null`, `false`, `0`, and `''` are
432
+ preserved. A root reviver result of `undefined`, or native `stringify(undefined)`,
433
+ does not invoke error handling or use the fallback. A string fallback or throwing
434
+ handler does not narrow stringify's public `string | undefined` return type.
435
+ Callback side effects are not rolled back. There is no logging, JSON repair, or
436
+ domain-shape validation.
437
+
438
+ ## Verification
439
+
440
+ `npm test` type-checks source and tests through `tsconfig.test.json`, then runs
441
+ Vitest. This includes compile-time `expectTypeOf` assertions. `npm run build` generates ESM,
442
+ CommonJS, and declarations; `npm run pack:check` inspects the npm package contents.