@lumjs/core 1.30.0 → 1.31.1

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/lib/obj/cp.js ADDED
@@ -0,0 +1,1375 @@
1
+ /**
2
+ * Object property copying utilities
3
+ * @module @lumjs/core/obj/cp
4
+ */
5
+ "use strict";
6
+
7
+ const {S,F,def,isObj,isComplex,isArray,isNil} = require('../types');
8
+
9
+ /// @see docs/src/obj-cp.js for type-defs and callbacks
10
+
11
+ /**
12
+ * A full set of type handlers for `cp`.
13
+ *
14
+ * @extends {Set}
15
+ * @alias module:@lumjs/core/obj/cp.HandlerSet
16
+ */
17
+ class HandlerSet extends Set
18
+ {
19
+ /**
20
+ * Add a type handler to this set.
21
+ * @param {module:@lumjs/core/obj/cp~Typ} type
22
+ *
23
+ * May alternatively be another `HandlerSet` instance,
24
+ * in which case all the type handlers in it will be added.
25
+ *
26
+ * @returns {module:@lumjs/core/obj/cp.HandlerSet} `this`
27
+ * @throws {TypeError} If `typedef` is not valid.
28
+ */
29
+ add(value)
30
+ {
31
+ if (value instanceof HandlerSet)
32
+ {
33
+ for (const th of value)
34
+ {
35
+ super.add(th);
36
+ }
37
+ return this;
38
+ }
39
+ else if ($TY.is(value))
40
+ {
41
+ return super.add(value);
42
+ }
43
+ else
44
+ {
45
+ console.debug({value, set: this});
46
+ throw new TypeError("Invalid TypeDef object");
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Find the handler for a given subject.
52
+ *
53
+ * If no explicit handler in this set supports the subject,
54
+ * the special `object` handler will be returned as a default.
55
+ *
56
+ * This method caches the result for each `subject` so that
57
+ * future requests don't have to run any tests.
58
+ *
59
+ * @param {object} subject
60
+ * @returns {module:@lumjs/core/obj/cp~Typ}
61
+ */
62
+ for(subject)
63
+ {
64
+ if (this.subCache === undefined)
65
+ {
66
+ def(this, 'subCache', {value: new Map()});
67
+ }
68
+
69
+ if (this.subCache.has(subject))
70
+ {
71
+ return this.subCache.get(subject);
72
+ }
73
+
74
+ for (const def of this)
75
+ {
76
+ if (def.test(subject))
77
+ { // Found one.
78
+ this.subCache.set(subject, def);
79
+ return def;
80
+ }
81
+ }
82
+
83
+ // No specific handler found, use the default.
84
+ this.subCache.set(subject, TY.object);
85
+ return TY.object;
86
+ }
87
+
88
+ /**
89
+ * Clears the subject cache used by `for()`
90
+ */
91
+ clearCache()
92
+ {
93
+ if (this.subCache instanceof Map)
94
+ {
95
+ this.subCache.clear();
96
+ }
97
+ }
98
+
99
+ } // HandlerSet class
100
+
101
+ /**
102
+ * Metadata and API methods for `cp.TY`
103
+ * @namespace
104
+ * @alias module:@lumjs/core/obj/cp.TY.self
105
+ */
106
+ const $TY =
107
+ {
108
+ /**
109
+ * Property names that will be skipped by `TY.for()` method
110
+ */
111
+ SKIP_FOR: ['for','object','self'],
112
+ /**
113
+ * Property names reserved for HandlerSet getter properties in `TY`
114
+ */
115
+ DEF_SETS: ['all','default'],
116
+ /**
117
+ * Mandatory methods (function properties) in a valid type handler.
118
+ */
119
+ NEED_FNS: ['test','new','clone','cpOver','cpSafe','getProps'],
120
+
121
+ /**
122
+ * Default type handler functions used by `make()` function
123
+ */
124
+ DEFAULTS:
125
+ {
126
+ clone(o,c)
127
+ {
128
+ return Object.assign(this.new(c), o);
129
+ },
130
+ cpOver: (o,s) => Object.assign(o, ...s),
131
+ cpSafe(o,ss,c)
132
+ {
133
+ for (const s of ss)
134
+ {
135
+ const ps = this.getProps(s,c);
136
+ for (const p in ps)
137
+ {
138
+ if (o[p] === undefined)
139
+ {
140
+ const d = ps[p];
141
+ def(o, p, d);
142
+ }
143
+ }
144
+ }
145
+ return o;
146
+ },
147
+ getProps(o,c)
148
+ {
149
+ const ap = Object.getOwnPropertyDescriptors(o)
150
+ if (c?.opts?.all) return ap;
151
+
152
+ const ep = {};
153
+ for (const p in ap)
154
+ {
155
+ if (ap[p].enumerable)
156
+ {
157
+ ep[p] = ap[p];
158
+ }
159
+ }
160
+ return ep;
161
+ },
162
+ extend()
163
+ {
164
+ return Object.assign({}, this, ...arguments);
165
+ },
166
+ }, // $TY.DEFAULTS
167
+
168
+ /**
169
+ * An array of reserved property names in `TY`;
170
+ * consists of `SKIP_FOR` and `DEF_SETS` combined.
171
+ */
172
+ get reserved()
173
+ {
174
+ return $TY.DEF_SETS.concat($TY.SKIP_FOR);
175
+ },
176
+
177
+ /**
178
+ * An array of the absolute minimum _required_ methods that **MUST**
179
+ * be included when calling `make()` or `add()`.
180
+ *
181
+ * It's `NEED_FNS` excluding any functions provided in `DEFAULTS`.
182
+ */
183
+ get requirements()
184
+ {
185
+ const defs = Object.keys($TY.DEFAULTS);
186
+ return $TY.NEED_FNS.filter(v => !defs.includes(v));
187
+ },
188
+
189
+ /**
190
+ * Make a new type handler definition
191
+ *
192
+ * @param {object} indef - Properties/functions for the handler
193
+ *
194
+ * Must contain at least functions for `test` and `new`.
195
+ * Any other mandatory functions will fallback on default versions.
196
+ *
197
+ * @return {module:@lumjs/core/obj/cp~Typ}
198
+ * @throws {TypeError} If any required properties/functions are missing.
199
+ */
200
+ make(indef)
201
+ {
202
+ const outdef = Object.assign({}, $TY.DEFAULTS, indef);
203
+ if (!$TY.is(outdef))
204
+ {
205
+ console.debug({indef, outdef, required: $TY.requirements});
206
+ throw new TypeError("Type handler is missing requirements");
207
+ }
208
+ return outdef;
209
+ },
210
+
211
+ /**
212
+ * Add a new _named_ type handler (or HandlerSet) to `cp.TY`
213
+ *
214
+ * @param {string} name - Name for the new type or set
215
+ *
216
+ * For types, the classname is generally a safe choice.
217
+ * Examples: `Set`, `Map`, `Element`, `NodeList`, etc.
218
+ * Or lowercase variants: `map`, `nodelist`, etc.
219
+ *
220
+ * For extensions of existing types, try to describe the changes.
221
+ * Examples: `arrayPush`, `lockedObject`, etc.
222
+ *
223
+ * For sets, something descriptive of what the set contains or does.
224
+ * Examples: `dom`, `addToLists`, etc.
225
+ *
226
+ * The name may not be any value in the `reserved` list!
227
+ *
228
+ * @param {object} obj - What we are adding
229
+ *
230
+ * If this is a `HandlerSet` object, or implements the complete
231
+ * [type handler interface]{@link module:@lumjs/core/obj/cp~Typ},
232
+ * it will be added directly.
233
+ *
234
+ * Anything else will be passed to
235
+ * {@link module:@lumjs/core/obj/cp.TY.self.make make()}
236
+ * to build a type handler.
237
+ *
238
+ * @returns {module:@lumjs/core/obj/cp.TY.self}
239
+ * So you can chain multiple .add() calls together.
240
+ *
241
+ * @throws {RangeError} If `name` was a reserved value
242
+ * @throws {TypeError} See `make()` for details
243
+ * @see module:@lumjs/core/obj/cp.TY.self.make
244
+ */
245
+ add(name, obj)
246
+ {
247
+ if ($TY.reserved.includes(name))
248
+ {
249
+ throw new RangeError(name+' is a reserved property name');
250
+ }
251
+ if (!(obj instanceof HandlerSet) && !$TY.is(obj))
252
+ {
253
+ obj = $TY.make(obj);
254
+ }
255
+ TY[name] = obj;
256
+ return this;
257
+ },
258
+
259
+ /**
260
+ * Is a value a valid type handler definition for `cp`?
261
+ * @param {*} v - Value to test
262
+ * @returns {boolean}
263
+ * @see module:@lumjs/core/obj/cp~Typ
264
+ */
265
+ is(v)
266
+ {
267
+ if (!isObj(v)) return false;
268
+
269
+ for (const need of $TY.NEED_FNS)
270
+ {
271
+ if (typeof v[need] !== F)
272
+ {
273
+ return false;
274
+ }
275
+ }
276
+
277
+ return true;
278
+ },
279
+
280
+ } // {$TY}
281
+
282
+ /**
283
+ * Type handler definitions for specific types of objects
284
+ * that require special handling when cloning or composing.
285
+ *
286
+ * @namespace
287
+ * @alias module:@lumjs/core/obj/cp.TY
288
+ */
289
+ const TY =
290
+ {
291
+ /**
292
+ * Handler for `object`
293
+ *
294
+ * This is a special implicit handler and does NOT need to *ever*
295
+ * be explicitly added to a `HandlerSet`, as it's always used as
296
+ * the *default fallback* if no other type handler matches.
297
+ */
298
+ object: $TY.make(
299
+ {
300
+ _id: 'object',
301
+ test: () => true,
302
+ new: () => ({}),
303
+ }),
304
+
305
+ /**
306
+ * Handler for `Array`
307
+ *
308
+ * It's `new()` method returns `[]`, and it's `clone()` method
309
+ * uses `subject.slice()` as a simple shallow array clone.
310
+ *
311
+ * No other array-specific behaviour is implemented in this very
312
+ * basic type handler, however it would be easy enough to extend
313
+ * it to add custom functionality (such as appending all sources
314
+ * into the subject for example).
315
+ *
316
+ * @type {module:@lumjs/core/obj/cp~Typ}
317
+ */
318
+ array: $TY.make(
319
+ {
320
+ _id: 'array',
321
+ test: isArray,
322
+ new: () => ([]),
323
+ clone: v => v.slice(),
324
+ }),
325
+
326
+ /**
327
+ * Assemble a complete Type Handler set
328
+ *
329
+ * @param {...(string|object)} types - Type handlers to include
330
+ *
331
+ * If this is a `string` it must be the name of a type handler
332
+ * property registered with the `TY` object itself.
333
+ *
334
+ * If this is an `object`, it must implement the
335
+ * {@link module:@lumjs/core/obj/cp~Typ} interface.
336
+ *
337
+ * @returns {module:@lumjs/core/obj/cp.HandlerSet}
338
+ * @throws {TypeError} If any of the `types` are not valid.
339
+ */
340
+ for(...types)
341
+ {
342
+ const defset = new HandlerSet();
343
+
344
+ for (const type of types)
345
+ { // First we'll add specified handlers.
346
+ if (typeof type === S
347
+ && !$TY.SKIP_FOR.includes(type)
348
+ && (type in this))
349
+ { // The name of one of our registered types.
350
+ defset.add(this[type]);
351
+ }
352
+ else if (type !== TY.object)
353
+ { // If it isn't a valid TypeDef, a TypeError will be thrown.
354
+ defset.add(type);
355
+ }
356
+ }
357
+
358
+ return defset;
359
+ },
360
+
361
+ /**
362
+ * A getter for a base HandlerSet with only `array` and `object` support.
363
+ *
364
+ * This getter creates a new instance every time it is accessed.
365
+ *
366
+ * @type {module:@lumjs/core/obj/cp.HandlerSet}
367
+ */
368
+ get base()
369
+ {
370
+ return new HandlerSet([this.array]);
371
+ },
372
+
373
+ /**
374
+ * A getter for a handler set with *all* of our registered
375
+ * type handlers included in it.
376
+ *
377
+ * Unless another library like `@lumjs/web-core` has added
378
+ * extra type handlers, this will end up being the same as
379
+ * `default` at this point in time.
380
+ *
381
+ * Like the `default` accessor property, this getter creates
382
+ * a new instance every time it is accessed.
383
+ *
384
+ * @type {module:@lumjs/core/obj/cp.HandlerSet}
385
+ */
386
+ get all()
387
+ {
388
+ const types = Object.keys(this)
389
+ .filter((name) => !$TY.reserved.includes(name))
390
+ .map((name) => this[name]);
391
+ return new HandlerSet(types);
392
+ },
393
+
394
+ } // {TY}
395
+
396
+ /**
397
+ * A globally shared default HandlerSet that will be used
398
+ * if no other is specified manually.
399
+ *
400
+ * Uses an instance of `cp.TY.base` as the default value.
401
+ * May be overridden with any valid HandlerSet.
402
+ *
403
+ * @type {module:@lumjs/core/obj/cp.HandlerSet}
404
+ * @alias module:@lumjs/core/obj/cp.TY.default
405
+ */
406
+ TY.default = TY.base;
407
+
408
+ function cpArgs(subject, ...sources)
409
+ {
410
+ let handlers;
411
+ if (subject instanceof HandlerSet)
412
+ {
413
+ handlers = subject;
414
+ subject = sources.shift();
415
+ }
416
+ else
417
+ {
418
+ handlers = TY.default;
419
+ }
420
+
421
+ const args = {subject, sources, handlers};
422
+
423
+ if (!isComplex(subject))
424
+ {
425
+ console.debug("Invalid subject", args);
426
+ args.invalid = true;
427
+ args.done = true;
428
+ return args;
429
+ }
430
+
431
+ args.th = handlers.for(subject);
432
+
433
+ if (sources.length === 0)
434
+ { // No sources? Just clone the subject.
435
+ args.subject = args.th.clone(subject);
436
+ args.done = true;
437
+ }
438
+
439
+ return args;
440
+ } // cpArgs()
441
+
442
+ /**
443
+ * Copy enumerable properties into a subject.
444
+ *
445
+ * This version overwrites any existing properties, with latter sources
446
+ * taking precedence over earlier ones.
447
+ *
448
+ * See {@link module:@lumjs/core/obj/cp.safe cp.safe()} for a version that
449
+ * only copies non-existent properties.
450
+ *
451
+ * @param {object} subject - Subject of the copy operation
452
+ *
453
+ * If this is a {@link module:@lumjs/core/obj/cp.HandlerSet HandlerSet},
454
+ * then it will be used instead of the default set, and the first
455
+ * item in `sources` will be re-assigned to the `subject` argument.
456
+ *
457
+ * If the `sources` have `0` items (after the HandlerSet logic obviously),
458
+ * then it indicates we want a shallow _clone_ of the subject. Which uses
459
+ * the {@link module:@lumjs/core/obj/cp~TypClone} method from the handler.
460
+ * Examples: `cp(myObj)` or `cp(myHandlers, myObj)`
461
+ *
462
+ * If you need to include *all* properties (not just *enumerable* ones),
463
+ * or need to do any kind of recursion or other advanced features, you'll
464
+ * need to use the declarative API. e.g. `cp.from(subject).all.clone()`
465
+ *
466
+ * @param {...object} [sources] Sources to copy into the subject
467
+ *
468
+ * This uses the {@link module:@lumjs/core/obj/cp~TypCpOver cpOver} method.
469
+ * The default version of which uses `Object.assign()` to copy properties
470
+ * from each source into the subject. Which may have odd results if used
471
+ * with certain kinds of objects.
472
+ *
473
+ * Like with cloning, if you need any advanced features, you'll need to
474
+ * use the declarative API. e.g. `cp.into(target).ow.deep.from(source)`
475
+ *
476
+ * @returns {object} Either `subject` or a clone of `subject`.
477
+ *
478
+ * @alias module:@lumjs/core/obj/cp.cp
479
+ *
480
+ * **Note**: this is the actual `obj.cp` default export value!
481
+ *
482
+ * To keep JSDoc happy it is listed as a _named export_ inside
483
+ * the `cp` module, but in reality, it *is* the module.
484
+ *
485
+ * So when you do: `const cp = require('@lumjs/core/obj/cp');`
486
+ * or `const core = require('@lumjs/core), {cp} = core.obj;`
487
+ * the `cp` variable will be this function. All the other named
488
+ * exports are properties attached to the function object itself.
489
+ *
490
+ * The named export *does* exist as an alias to itself (`cp.cp = cp`).
491
+ *
492
+ */
493
+ function cp()
494
+ {
495
+ const args = cpArgs(...arguments);
496
+ if (args.done) return args.subject;
497
+ return args.th.cpOver(args.subject, args.sources);
498
+ }
499
+
500
+ /**
501
+ * Copy non-existent enumerable properties into a subject.
502
+ *
503
+ * This is a version of {@link module:@lumjs/core/obj/cp.cp cp()} that
504
+ * uses the {@link module:@lumjs/core/obj/cp~TypCpSafe cpSafe} method
505
+ * when copying properties so it does not overwrite existing properties.
506
+ *
507
+ * Other than that difference, the rest of the arguments and their
508
+ * associated behaviours are identical to `cp()`, so see that for details.
509
+ *
510
+ * @param {object} subject
511
+ * @param {...object} sources
512
+ * @returns {object}
513
+ * @alias module:@lumjs/core/obj/cp.safe
514
+ */
515
+ cp.safe = function()
516
+ {
517
+ const args = cpArgs(...arguments);
518
+ if (args.done) return args.subject;
519
+ return args.th.cpSafe(args.subject, args.sources);
520
+ }
521
+
522
+ /**
523
+ * Make a clone of an object using JSON
524
+ *
525
+ * Serialize an object to JSON, then parse it back into an object.
526
+ * It's a ridiculously simple method for simple _deep_ cloning,
527
+ * but comes with many limitations inherent to JSON serialization.
528
+ *
529
+ * @param {object} obj - Object to clone
530
+ * @param {object} [opts] Advanced options
531
+ * @param {function} [opts.replace] A JSON _replacer_ function
532
+ * @param {function} [opts.revive] A JSON _reviver_ function
533
+ * @returns {object} A clone of the `obj`
534
+ * @alias module:@lumjs/core/obj/cp.json
535
+ */
536
+ cp.json = function(obj, opts={})
537
+ {
538
+ if (!isObj(obj))
539
+ {
540
+ console.warn("cp.json() does not support non-object");
541
+ return obj;
542
+ }
543
+
544
+ return JSON.parse(JSON.stringify(obj, opts.replace), opts.revive);
545
+ }
546
+
547
+ const DEFAULT_CPAPI_OPTS =
548
+ {
549
+ all: false,
550
+ overwrite: false,
551
+ recursive: 0,
552
+ toggleDepth: -1,
553
+ proto: false,
554
+ }
555
+
556
+ const VALIDATION =
557
+ {
558
+ handlers: v => v instanceof HandlerSet,
559
+ onUpdate: v => typeof v === F,
560
+ }
561
+
562
+ for (const opt in DEFAULT_CPAPI_OPTS)
563
+ { // Generate some default validation rules based on type values.
564
+ if (VALIDATION[opt] === undefined)
565
+ {
566
+ const vt = typeof DEFAULT_CPAPI_OPTS[opt];
567
+ VALIDATION[opt] = v => typeof(v) === vt;
568
+ }
569
+ }
570
+
571
+ const CPAPI_TOGGLE_OPTS = ['deep','ow'];
572
+
573
+ /**
574
+ * Context object for use in the declarative API.
575
+ *
576
+ * May have additional properties added by `opts.onUpdate()` if used.
577
+ * This documentation lists only standard properties that will be
578
+ * used without any custom options.
579
+ *
580
+ * Properties marked with **¿** will only be included when applicable.
581
+ *
582
+ * Properties marked with **¡** may be set automatically when applicable.
583
+ * Currently that only applies to type handlers, which will be looked
584
+ * up using `opts.handlers.for()` as a convenience shortcut.
585
+ *
586
+ * @prop {module:@lumjs/core/obj/cp.API} cp - API instance
587
+ * @prop {object} opts - Current options; defaults to `cp.opts`
588
+ * @prop {number} depth - Current recusion depth (`0` is top-level)
589
+ * @prop {?object} prev - Previous context (if applicable)
590
+ *
591
+ * Will be `null` unless if `opts.recusive` is not `0` and the
592
+ * context is for a nested operation (top-level will always be `null`).
593
+ *
594
+ * @prop {object} [target] Current target **¿**
595
+ * @prop {object} [source] Current source **¿**
596
+ * @prop {number} [ti] Current target index **¿**
597
+ * @prop {number} [si] Current source index **¿**
598
+ * @prop {module:@lumjs/core/obj/cp~Typ} [th] Handler for `target` **¡**
599
+ * @prop {module:@lumjs/core/obj/cp~Typ} [sh] Handler for `source` **¡**
600
+ *
601
+ * @prop {(string|symbol)} [prop] Property being operated on **¿**
602
+ * @prop {object} [td] Target descriptor for `prop` **¿**
603
+ * @prop {object} [sd] Source descriptor for `prop` **¿**
604
+ *
605
+ * @alias module:@lumjs/core/obj/cp~Context
606
+ */
607
+ class CPContext
608
+ {
609
+ /**
610
+ * Build a new Context object.
611
+ *
612
+ * Generally this is only called by the `cp.getContext()` API method.
613
+ *
614
+ * This will call `this.update(prev, data)` after setting
615
+ * `this.cp` and `this.opts` to their default values.
616
+ *
617
+ * Lastly it sets `this.prev` overwriting any previous value.
618
+ *
619
+ * @param {module:@lumjs/core/obj/cp.API} api - API instance
620
+ * @param {object} data - Initial data
621
+ * @param {?module:@lumjs/core/obj/cp~Context} prev - Previous context
622
+ */
623
+ constructor(api, data, prev)
624
+ {
625
+ this.cp = api;
626
+ this.opts = api.opts;
627
+ this.depth = 0;
628
+ this.update(prev, data);
629
+ this.prev = prev;
630
+ }
631
+
632
+ /**
633
+ * Update the context with new data
634
+ *
635
+ * Will automatically populate certain properties depending on
636
+ * the data added.
637
+ *
638
+ * @param {...object} data - Properties to add to the context
639
+ *
640
+ * Nothing is
641
+ *
642
+ * @returns {object} `this`
643
+ */
644
+ update()
645
+ {
646
+ Object.assign(this, ...arguments);
647
+
648
+ if (typeof this.opts.onUpdate === F)
649
+ {
650
+ this.opts.onUpdate(this, ...arguments);
651
+ }
652
+
653
+ if (this.target && !this.th)
654
+ {
655
+ this.th = this.opts.handlers.for(this.target);
656
+ }
657
+
658
+ if (this.source && !this.sh)
659
+ {
660
+ this.sh = this.opts.handlers.for(this.source);
661
+ }
662
+
663
+ return this;
664
+ }
665
+
666
+ /**
667
+ * Set context options
668
+ *
669
+ * It makes a _new object_ composing the original `this.opts` and `changes`,
670
+ * and sets that to `this.opts`. It does not change the original opts object.
671
+ *
672
+ * This is meant to be called from custom `onUpdate` handler functions,
673
+ * and isn't actually used by anything in the library itself.
674
+ *
675
+ * @param {object} changes - Any option values you want to change.
676
+ *
677
+ * @returns {object} `this`
678
+ */
679
+ setOpts(changes)
680
+ {
681
+ this.opts = Object.assign({}, this.opts, changes);
682
+ return this;
683
+ }
684
+
685
+ } // CPContext class
686
+
687
+ /**
688
+ * A class providing a declarative API for advanced `obj.cp` API calls.
689
+ *
690
+ * This is based off of the earlier `copyProps` declarative API,
691
+ * and is meant to replace it entirely in future releases of this library.
692
+ *
693
+ * @alias module:@lumjs/core/obj/cp.API
694
+ */
695
+ class CPAPI
696
+ {
697
+ /**
698
+ * Create a declarative `obj.cp` API instance.
699
+ *
700
+ * You generally would never call this directly, but use one of the
701
+ * functions in `cp` instead, a few examples:
702
+ *
703
+ * ```js
704
+ * // Copy properties recursively, allowing overwrite
705
+ * cp.into(target).ow.deep.from(source);
706
+ *
707
+ * // Clone an object, including all (not just enumerable) properties
708
+ * let cloned = cp.from(obj).all.clone();
709
+ *
710
+ * ```
711
+ *
712
+ * @param {(object|function)} [opts] Options
713
+ *
714
+ * If this is a `function` it will be used as the `opts.onUpdate` value.
715
+ *
716
+ * If this is a {@link module:@lumjs/core/obj/cp.HandlerSet} object,
717
+ * it will be used as the `opts.handlers` value.
718
+ *
719
+ * @param {module:@lumjs/core/obj/cp.HandlerSet} [opts.handlers]
720
+ * Specific type handlers to use with this API instance.
721
+ *
722
+ * If not specified, the default set will be used.
723
+ *
724
+ * @param {boolean} [opts.all=false] Include all properties?
725
+ *
726
+ * If `false` (default), we'll only use *enumerable* properties.
727
+ * If `true` we'll include *all* property descriptors in the subject.
728
+ *
729
+ * @param {boolean} [opts.overwrite=false] Overwrite existing properties?
730
+ *
731
+ * @param {boolean} [opts.proto=false] Copy prototype?
732
+ *
733
+ * If `true`, the prototype on each target will be set to the
734
+ * prototype of the first source. This is `false` by default.
735
+ *
736
+ * @param {number} [opts.recursive=0] Recurse into nested objects?
737
+ *
738
+ * - `0` → disables recursion entirely (default)
739
+ * - `> 0` → specify depth recursion should be allowed
740
+ * - `< 0` → unlimited recursion depth
741
+ *
742
+ * When recursion is enabled, we will cache nested objects that we've
743
+ * already processed, so we don't get stuck in an infinite loop.
744
+ *
745
+ * @param {number} [opts.toggleDepth=-1] This is the recursion depth
746
+ * that will be used when the `deep` toggle value is `true`.
747
+ *
748
+ * @param {function} [opts.onUpdate] A custom function
749
+ */
750
+ constructor(opts={})
751
+ {
752
+ if (typeof opts === F)
753
+ {
754
+ opts = {onUpdate: opts, handlers: TY.default};
755
+ }
756
+ else if (opts instanceof HandlerSet)
757
+ {
758
+ opts = {handlers: opts};
759
+ }
760
+ else if (!(opts.handlers instanceof HandlerSet))
761
+ {
762
+ opts.handlers = TY.default;
763
+ }
764
+
765
+ this.opts = Object.assign({}, DEFAULT_CPAPI_OPTS);
766
+ this.set(opts);
767
+
768
+ this.sources = [];
769
+ this.targets = [];
770
+
771
+ this.nextToggle = true;
772
+ }
773
+
774
+ /**
775
+ * Get a context object to pass to type handler methods
776
+ * @param {object} data - Initial data for the context object
777
+ * @param {object} [prev] A previous context object (if applicable)
778
+ * @returns {module:@lumjs/core/obj/cp~Context}
779
+ */
780
+ getContext(data, prev=null)
781
+ {
782
+ return new CPContext(this, data, prev);
783
+ }
784
+
785
+ /**
786
+ * Use specific type handlers or a custom context onUpdate function.
787
+ *
788
+ * @param {(module:@lumjs/core/obj/cp.HandlerSet|function)} arg
789
+ *
790
+ * If this is a `function`, we will set `opts.onUpdate` using it.
791
+ * Otherwise we will set `opts.handlers`.
792
+ *
793
+ * @returns {object} `this`
794
+ * @throws {TypeError} If `arg` is invalid
795
+ */
796
+ use(arg)
797
+ {
798
+ const opt = typeof arg === F ? 'onUpdate' : 'handlers';
799
+ return this.set({[opt]: arg}, true);
800
+ }
801
+
802
+ /**
803
+ * The next *toggle accessor property* will be set to `false`.
804
+ *
805
+ * This only affects the very next toggle, after which the
806
+ * toggle value will return to its default value of `true`.
807
+ *
808
+ * You need to explicitly use `not` before each toggle property
809
+ * that you want to turn off instead of on.
810
+ */
811
+ get not()
812
+ {
813
+ this.nextToggle = false;
814
+ return this;
815
+ }
816
+
817
+ /**
818
+ * Private method that handles all *toggle properties*.
819
+ * @private
820
+ * @param {string} opt - Name of option to toggle
821
+ * @param {*} [trueVal=true] Value to use if toggle is `true`
822
+ * @param {*} [falseVal=false] Value to use if toggle is `false`
823
+ * @returns {object} `this`
824
+ */
825
+ $toggle(opt, trueVal=true, falseVal=false)
826
+ {
827
+ this.opts[opt] = this.nextToggle ? trueVal : falseVal;
828
+ this.nextToggle = true;
829
+ return this;
830
+ }
831
+
832
+ /**
833
+ * Toggle `opts.all` with boolean value when accessed
834
+ */
835
+ get all()
836
+ {
837
+ return this.$toggle('all');
838
+ }
839
+
840
+ /**
841
+ * Toggle `opts.overwrite` with boolean value when accessed
842
+ */
843
+ get ow()
844
+ {
845
+ return this.$toggle('overwrite');
846
+ }
847
+
848
+ /**
849
+ * Toggle `opts.recursive` when accessed
850
+ *
851
+ * Value to set depends on the toggle value:
852
+ *
853
+ * `true` → `opts.recursive = opts.toggleDepth`
854
+ * `false` → `opts.recursive = 0`
855
+ *
856
+ */
857
+ get deep()
858
+ {
859
+ const max = this.opts.toggleDepth;
860
+ if (max === 0) console.error("toggleDepth is 0", {cp: this});
861
+ return this.$toggle('recursive', max, 0);
862
+ }
863
+
864
+ /**
865
+ * Set specified options
866
+ * @param {object} opts - Options to set
867
+ * @param {boolean} [fatal=false] Throw on invalid type?
868
+ *
869
+ * By default we report invalid option values to the console,
870
+ * and simply skip setting the invalid option.
871
+ *
872
+ * If this is `true` any invalid option values will result in
873
+ * an error being thrown, ending the entire `set()` process.
874
+ *
875
+ * @returns {object} `this`
876
+ * @throws {TypeError} See `fatal` argument for details
877
+ */
878
+ set(opts, fatal=false)
879
+ {
880
+ if (isObj(opts))
881
+ {
882
+ for (const opt in opts)
883
+ {
884
+ if (opt in VALIDATION)
885
+ {
886
+ if (!VALIDATION[opt](opts[opt]))
887
+ {
888
+ const msg = 'invalid option value';
889
+ const info = {opt, opts, cp: this};
890
+ if (fatal)
891
+ {
892
+ console.error(info);
893
+ throw new TypeError(msg);
894
+ }
895
+ else
896
+ {
897
+ console.error(msg, info);
898
+ continue;
899
+ }
900
+ }
901
+ }
902
+ else if (opt in CPAPI_TOGGLE_OPTS)
903
+ {
904
+ if (!opts[opt])
905
+ { // Negate the toggle.
906
+ this.not;
907
+ }
908
+ // Now toggle it.
909
+ this[opt];
910
+ continue;
911
+ }
912
+
913
+ this.opts[opt] = opts[opt];
914
+ }
915
+ }
916
+ else
917
+ {
918
+ console.error("invalid opts", {opts, cp: this});
919
+ }
920
+ return this;
921
+ }
922
+
923
+ /**
924
+ * Specify the `targets` to copy properties into.
925
+ *
926
+ * @param {...object} [targets] The target objects
927
+ *
928
+ * If you don't specify any targets, then `this.targets`
929
+ * will be cleared of any existing objects.
930
+ *
931
+ * If `this.sources` has objects in it, then we'll copy
932
+ * those sources into each of the specified targets.
933
+ *
934
+ * If `this.sources` is empty, then this will set
935
+ * `this.targets` to the specified value.
936
+ *
937
+ * @returns {object} `this`
938
+ */
939
+ into(...targets)
940
+ {
941
+ if (this.sources.length > 0 && targets.length > 0)
942
+ {
943
+ this.$runAll(this.sources, targets);
944
+ }
945
+ else
946
+ {
947
+ this.targets = targets;
948
+ }
949
+ return this;
950
+ }
951
+
952
+ /**
953
+ * Specify the `sources` to copy properties from.
954
+ *
955
+ * @param {...object} [sources] The source objects.
956
+ *
957
+ * If you don't specify any sources, then `this.sources`
958
+ * will be cleared of any existing objects.
959
+ *
960
+ * If `this.targets` has objects in it, then we'll copy
961
+ * the specified sources into each of those targets.
962
+ *
963
+ * If `this.targets` is empty, then this will set
964
+ * `this.sources` to the specified value.
965
+ *
966
+ * @returns {object} `this`
967
+ */
968
+ from(...sources)
969
+ {
970
+ if (this.targets.length > 0 && sources.length > 0)
971
+ {
972
+ this.$runAll(sources, this.targets);
973
+ }
974
+ else
975
+ {
976
+ this.sources = sources;
977
+ }
978
+ return this;
979
+ }
980
+
981
+ /**
982
+ * Add additional subjects
983
+ *
984
+ * - If you started with `from()` this adds to the sources.
985
+ * - If you started with `into()` this adds to the targets.
986
+ *
987
+ * @param {...object} subjects - More sources or targets to add
988
+ * @returns {object} `this`
989
+ */
990
+ and(...subjects)
991
+ {
992
+ if (this.sources.length > 0)
993
+ {
994
+ this.sources.push(...subjects);
995
+ }
996
+ else if (this.targets.length > 0)
997
+ {
998
+ this.targets.push(...subjects);
999
+ }
1000
+ else
1001
+ {
1002
+ console.error("No sources or targets set", {subjects, cp: this});
1003
+ }
1004
+ return this;
1005
+ }
1006
+
1007
+ /**
1008
+ * See if we can use EZ mode.
1009
+ *
1010
+ * EZ-mode is a shortcut to use `cp()` or `cp.safe()`
1011
+ * to perform copy operations if applicable.
1012
+ */
1013
+ get isEZ()
1014
+ {
1015
+ const o = this.opts;
1016
+ return (!o.all && !o.recursive);
1017
+ }
1018
+
1019
+ /**
1020
+ * Clone the properties of the sources into a new object.
1021
+ *
1022
+ * This will only with if `cp.into()` was used to create the
1023
+ * API instance. It will throw an error if `cp.from()` was used.
1024
+ *
1025
+ * This gets the type handler for the first source, uses it
1026
+ * to get a new object, then runs the copy operation with the
1027
+ * new object as the sole target.
1028
+ *
1029
+ * @returns {object} A new object
1030
+ * @throws {RangeError} If there are targets set instead of sources
1031
+ */
1032
+ clone()
1033
+ {
1034
+ if (this.targets.length > 0)
1035
+ {
1036
+ console.debug(this);
1037
+ throw new RangeError("cannot clone when targets set");
1038
+ }
1039
+
1040
+ if (this.sources.length === 0)
1041
+ {
1042
+ console.debug(this);
1043
+ throw new RangeError("cannot clone with no sources");
1044
+ }
1045
+
1046
+ // Get the type handler for the first source.
1047
+ const sh = this.opts.handlers.for(this.sources[0]);
1048
+ const target = sh.new();
1049
+
1050
+ this.$runAll(this.sources, [target]);
1051
+
1052
+ return target;
1053
+ }
1054
+
1055
+ /**
1056
+ * Run a copy operation for the specified sources and targets.
1057
+ *
1058
+ * For _each target_, it will find the type handler, then do one of:
1059
+ *
1060
+ * - If `this.isEZ` is `true`, calls `cp()` or `cp.safe()` to perform
1061
+ * the operation, and no further processing will be required.
1062
+ *
1063
+ * - If the type handler has a `copyAll()` method,
1064
+ * that will be called, passing the `sources` array
1065
+ * in its entirety.
1066
+ *
1067
+ * - If the type handler instead has a `copyOne()` method,
1068
+ * that will be called once for each source.
1069
+ *
1070
+ * - If the handler has neither of those, then `this.$runOne()`
1071
+ * will be called once for each source.
1072
+ *
1073
+ * @private
1074
+ * @param {Array} sources
1075
+ * @param {Array} targets
1076
+ * @returns {void}
1077
+ */
1078
+ $runAll(sources, targets)
1079
+ {
1080
+ const ez = this.isEZ;
1081
+ for (const ti in targets)
1082
+ {
1083
+ const target = targets[ti];
1084
+
1085
+ if (ez)
1086
+ { // EZ mode, use one of the simple functions.
1087
+ const hdl = this.opts.handlers;
1088
+ const fun = this.opts.overwrite ? cp : cp.safe;
1089
+ fun(hdl, target, ...sources);
1090
+ }
1091
+ else
1092
+ { // Advanced mode supports many more options.
1093
+ const ctx = this.getContext({target,ti,cache: []});
1094
+ //console.debug({target, ti, ctx});
1095
+ const {th} = ctx;
1096
+ if (typeof th.copyAll === F)
1097
+ { // Type handler will handle all sources at once.
1098
+ th.copyAll(ctx, sources);
1099
+ }
1100
+ else
1101
+ { // One source at a time.
1102
+ const isHandled = (typeof th.copyOne === F);
1103
+ for (const si in sources)
1104
+ {
1105
+ const source = sources[si];
1106
+ ctx.update({source,si});
1107
+
1108
+ if (isHandled)
1109
+ {
1110
+ th.copyOne(ctx);
1111
+ }
1112
+ else
1113
+ {
1114
+ this.$runOne(ctx);
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+ }
1120
+ } // $runAll()
1121
+
1122
+ /**
1123
+ * Run a copy operation from a single source into a single target.
1124
+ * This is called by `$runAll()`, and also calls itself recursively
1125
+ * if `opts.recursive` is not set to `0` (its default value).
1126
+ *
1127
+ * @private
1128
+ * @param {module:@lumjs/core/obj/cp~Context} ctx - Context;
1129
+ * must have both `target` and `source` properties defined.
1130
+ * @returns {void}
1131
+ */
1132
+ $runOne(rc)
1133
+ {
1134
+ const {target,source,cache} = rc;
1135
+ const sprops = rc.sh.getProps(source, rc);
1136
+ const tprops = rc.th.getProps(target, rc);
1137
+
1138
+ for (const prop in sprops)
1139
+ {
1140
+ const sd = sprops[prop];
1141
+ let td = tprops[prop];
1142
+
1143
+ const pc = this.getContext({prop,sd}, rc);
1144
+ const po = pc.opts;
1145
+
1146
+ if ((po.recursive < 0 || (po.recursive > pc.depth))
1147
+ && isObj(sd.value)
1148
+ && !cache.includes(sd.value)
1149
+ )
1150
+ { // A nested object was found.
1151
+ //console.debug("recursive mode", {sd,td});
1152
+ cache.push(sd.value);
1153
+ if (sd.value !== td?.value)
1154
+ { // Not the same literal object.
1155
+ pc.update({sh: null, source: sd.value});
1156
+
1157
+ if (isObj(td))
1158
+ { // Use the existing target descriptor
1159
+ pc.update({td});
1160
+ }
1161
+ else
1162
+ { // Create a new descriptor.
1163
+ const value = pc.sh.new();
1164
+ //console.debug({ph: pctx.sh ,value});
1165
+ td = Object.assign({}, sd, {value});
1166
+ pc.update({td});
1167
+ def(target, prop, pc.td);
1168
+ }
1169
+
1170
+ //cache.push(td.value);
1171
+ pc.update({th: null, target: td.value, depth: pc.depth+1});
1172
+
1173
+ this.$runOne(pc);
1174
+ }
1175
+ }
1176
+ else if (po.overwrite || td === undefined)
1177
+ {
1178
+ def(target, prop, pc.sd);
1179
+ }
1180
+ }
1181
+
1182
+ if (rc.si === 0 && rc.opts.proto)
1183
+ { // Prototype assignment only done with first source.
1184
+ const sp = Object.getPrototypeOf(source);
1185
+ const tp = Object.getPrototypeOf(target);
1186
+ if (sp && sp !== tp)
1187
+ { // Apply the source prototype to the target.
1188
+ Object.setPrototypeOf(target, sp);
1189
+ }
1190
+ }
1191
+
1192
+ } // $runOne()
1193
+
1194
+ } // CPAPI class
1195
+
1196
+ /**
1197
+ * Create a new `cp` declarative API instance with the specified options.
1198
+ * @param {(function|object)} opts - Passed to the constructor
1199
+ * @returns {module:@lumjs/core/obj/cp.API} API instance
1200
+ * @alias module:@lumjs/core/obj/cp.with
1201
+ */
1202
+ cp.with = function(opts)
1203
+ {
1204
+ return new CPAPI(opts);
1205
+ }
1206
+
1207
+ /**
1208
+ * Create a new `cp` declarative API instance with default options,
1209
+ * and the specified target objects.
1210
+ * @param {...objects} targets - Target objects
1211
+ * @returns {module:@lumjs/core/obj/cp.API} API instance
1212
+ * @alias module:@lumjs/core/obj/cp.into
1213
+ */
1214
+ cp.into = function()
1215
+ {
1216
+ return (new CPAPI()).into(...arguments);
1217
+ }
1218
+
1219
+ /**
1220
+ * Create a new `cp` declarative API instance with default options,
1221
+ * and the specified source objects.
1222
+ * @param {...objects} source - Source objects
1223
+ * @returns {module:@lumjs/core/obj/cp.API} API instance
1224
+ * @alias module:@lumjs/core/obj/cp.from
1225
+ */
1226
+ cp.from = function()
1227
+ {
1228
+ return (new CPAPI()).from(...arguments);
1229
+ }
1230
+
1231
+ /**
1232
+ * A wrapper around `cp` specifically for cloning.
1233
+ *
1234
+ * @param {object} obj - Object to clone
1235
+ *
1236
+ * @param {?object} [opts] Options
1237
+ *
1238
+ * If this is an `object`, the clone call will be:
1239
+ * `cp.with(opts).into(obj).ow.clone();`
1240
+ *
1241
+ * If this is `null` or `undefined`, the call will be:
1242
+ * `cp(obj);`
1243
+ *
1244
+ * @returns {object} `obj`
1245
+ * @alias module:@lumjs/core/obj/cp.clone
1246
+ */
1247
+ cp.clone = function(obj, opts)
1248
+ {
1249
+ if (isNil(opts))
1250
+ {
1251
+ return cp(obj);
1252
+ }
1253
+ else
1254
+ {
1255
+ return cp.with(opts).into(obj).ow.clone();
1256
+ }
1257
+ }
1258
+
1259
+ /**
1260
+ * Add a clone() method to an object.
1261
+ *
1262
+ * The method added is pretty simple with a very basic signature:
1263
+ * `obj.clone(opts)`, and it calls `cp.clone(obj,opts);`
1264
+ *
1265
+ * @param {object} obj - Object to add method to
1266
+ * @param {object} [spec] Optional spec
1267
+ * @param {string} [spec.name='clone'] Method name to add;
1268
+ * Default: `clone`
1269
+ * @param {object} [spec.desc] Descriptor rules
1270
+ * @param {?object} [spec.opts] Default options for method;
1271
+ * Default is `null` so that simple cloning is the default.
1272
+ *
1273
+ * @returns {object} `obj`
1274
+ * @alias module:@lumjs/core/obj/cp.addClone
1275
+ */
1276
+ function addClone(obj, spec={})
1277
+ {
1278
+ const name = spec.name ?? 'clone';
1279
+ const desc = spec.desc ?? {};
1280
+ const defs = spec.opts ?? null;
1281
+
1282
+ desc.value = function(opts)
1283
+ {
1284
+ if (isObj(defs) && opts !== null)
1285
+ {
1286
+ opts = Object.assign({}, defs, opts);
1287
+ }
1288
+
1289
+ return cp.clone(obj, opts);
1290
+ }
1291
+
1292
+ return def(obj, name, desc);
1293
+ }
1294
+
1295
+ /**
1296
+ * A singleton object offering cached `cp.API` instances.
1297
+ * @alias module:@lumjs/core/obj/cp.cache
1298
+ */
1299
+ cp.cache =
1300
+ {
1301
+ /**
1302
+ * Return a `cp` instance with a single `target` object.
1303
+ *
1304
+ * Will cache the instance the first time, so future calls
1305
+ * with the same `target` will return the same instance.
1306
+ *
1307
+ * @param {object} target
1308
+ * @returns {module:@lumjs/core/obj/cp.API}
1309
+ */
1310
+ into(target)
1311
+ {
1312
+ if (this.intoCache === undefined)
1313
+ this.intoCache = new Map();
1314
+ const cache = this.intoCache;
1315
+ if (cache.has(target))
1316
+ {
1317
+ return cache.get(target);
1318
+ }
1319
+ else
1320
+ {
1321
+ const api = cp.into(target);
1322
+ cache.set(target, api);
1323
+ return api;
1324
+ }
1325
+ },
1326
+ /**
1327
+ * Return a `cp` instance with a single `source` object.
1328
+ *
1329
+ * Will cache the instance the first time, so future calls
1330
+ * with the same `target` will return the same instance.
1331
+ *
1332
+ * @param {object} source
1333
+ * @returns {module:@lumjs/core/obj/cp.API}
1334
+ */
1335
+ from(source)
1336
+ {
1337
+ if (this.fromCache === undefined)
1338
+ this.fromCache = new Map();
1339
+ const cache = this.fromCache;
1340
+ if (cache.has(source))
1341
+ {
1342
+ return cache.get(source);
1343
+ }
1344
+ else
1345
+ {
1346
+ const api = cp.from(source);
1347
+ cache.set(source, api);
1348
+ return api;
1349
+ }
1350
+ },
1351
+ /**
1352
+ * Clear the caches for `into` and `from`.
1353
+ * @returns {object} `cp.cache`
1354
+ */
1355
+ clear()
1356
+ {
1357
+ if (this.intoCache)
1358
+ this.intoCache.clear();
1359
+ if (this.fromCache)
1360
+ this.fromCache.clear();
1361
+ return this;
1362
+ },
1363
+ } // cp.cache
1364
+
1365
+ // Assign the rest of the named exports
1366
+ Object.assign(cp,
1367
+ {
1368
+ addClone, TY, HandlerSet,
1369
+ API: CPAPI,
1370
+ Context: CPContext,
1371
+ __cpArgs: cpArgs,
1372
+ });
1373
+
1374
+ // Export the module itself, including self reference.
1375
+ module.exports = cp.cp = cp;