@lumjs/core 1.38.4 → 1.38.5

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 CHANGED
@@ -1,1374 +1,8 @@
1
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
- * **Note**: this is the actual `obj.cp` default export value!
452
- *
453
- * To keep JSDoc happy it is listed as a _named export_ inside
454
- * the `cp` module, but in reality, it *is* the module.
455
- *
456
- * So when you do: `const cp = require('@lumjs/core/obj/cp');`
457
- * or `const core = require('@lumjs/core), {cp} = core.obj;`
458
- * the `cp` variable will be this function. All the other named
459
- * exports are properties attached to the function object itself.
460
- *
461
- * The named export *does* exist as an alias to itself (`cp.cp = cp`).
462
- *
463
- * @param {object} subject - Subject of the copy operation
464
- *
465
- * If this is a {@link module:@lumjs/core/obj/cp.HandlerSet HandlerSet},
466
- * then it will be used instead of the default set, and the first
467
- * item in `sources` will be re-assigned to the `subject` argument.
468
- *
469
- * If the `sources` have `0` items (after the HandlerSet logic obviously),
470
- * then it indicates we want a shallow _clone_ of the subject. Which uses
471
- * the {@link module:@lumjs/core/obj/cp~TypClone} method from the handler.
472
- * Examples: `cp(myObj)` or `cp(myHandlers, myObj)`
473
- *
474
- * If you need to include *all* properties (not just *enumerable* ones),
475
- * or need to do any kind of recursion or other advanced features, you'll
476
- * need to use the declarative API. e.g. `cp.from(subject).all.clone()`
477
- *
478
- * @param {...object} [sources] Sources to copy into the subject
479
- *
480
- * This uses the {@link module:@lumjs/core/obj/cp~TypCpOver cpOver} method.
481
- * The default version of which uses `Object.assign()` to copy properties
482
- * from each source into the subject. Which may have odd results if used
483
- * with certain kinds of objects.
484
- *
485
- * Like with cloning, if you need any advanced features, you'll need to
486
- * use the declarative API. e.g. `cp.into(target).ow.deep.from(source)`
487
- *
488
- * @returns {object} Either `subject` or a clone of `subject`.
489
- *
490
- * @alias module:@lumjs/core/obj/cp.cp
491
- */
492
- function cp()
493
- {
494
- const args = cpArgs(...arguments);
495
- if (args.done) return args.subject;
496
- return args.th.cpOver(args.subject, args.sources);
497
- }
498
-
499
- /**
500
- * Copy non-existent enumerable properties into a subject.
501
- *
502
- * This is a version of {@link module:@lumjs/core/obj/cp.cp cp()} that
503
- * uses the {@link module:@lumjs/core/obj/cp~TypCpSafe cpSafe} method
504
- * when copying properties so it does not overwrite existing properties.
505
- *
506
- * Other than that difference, the rest of the arguments and their
507
- * associated behaviours are identical to `cp()`, so see that for details.
508
- *
509
- * @param {object} subject
510
- * @param {...object} sources
511
- * @returns {object}
512
- * @alias module:@lumjs/core/obj/cp.safe
513
- */
514
- cp.safe = function()
515
- {
516
- const args = cpArgs(...arguments);
517
- if (args.done) return args.subject;
518
- return args.th.cpSafe(args.subject, args.sources);
519
- }
520
-
521
- /**
522
- * Make a clone of an object using JSON
2
+ * Object property copying utilities.
523
3
  *
524
- * Serialize an object to JSON, then parse it back into an object.
525
- * It's a ridiculously simple method for simple _deep_ cloning,
526
- * but comes with many limitations inherent to JSON serialization.
4
+ * NOTE: This now just re-exports the `@lumjs/cp` package.
527
5
  *
528
- * @param {object} obj - Object to clone
529
- * @param {object} [opts] Advanced options
530
- * @param {function} [opts.replace] A JSON _replacer_ function
531
- * @param {function} [opts.revive] A JSON _reviver_ function
532
- * @returns {object} A clone of the `obj`
533
- * @alias module:@lumjs/core/obj/cp.json
534
- */
535
- cp.json = function(obj, opts={})
536
- {
537
- if (!isObj(obj))
538
- {
539
- console.warn("cp.json() does not support non-object");
540
- return obj;
541
- }
542
-
543
- return JSON.parse(JSON.stringify(obj, opts.replace), opts.revive);
544
- }
545
-
546
- const DEFAULT_CPAPI_OPTS =
547
- {
548
- all: false,
549
- overwrite: false,
550
- recursive: 0,
551
- toggleDepth: -1,
552
- proto: false,
553
- }
554
-
555
- const VALIDATION =
556
- {
557
- handlers: v => v instanceof HandlerSet,
558
- onUpdate: v => typeof v === F,
559
- }
560
-
561
- for (const opt in DEFAULT_CPAPI_OPTS)
562
- { // Generate some default validation rules based on type values.
563
- if (VALIDATION[opt] === undefined)
564
- {
565
- const vt = typeof DEFAULT_CPAPI_OPTS[opt];
566
- VALIDATION[opt] = v => typeof(v) === vt;
567
- }
568
- }
569
-
570
- const CPAPI_TOGGLE_OPTS = ['deep','ow'];
571
-
572
- /**
573
- * Context object for use in the declarative API.
574
- *
575
- * May have additional properties added by `opts.onUpdate()` if used.
576
- * This documentation lists only standard properties that will be
577
- * used without any custom options.
578
- *
579
- * Properties marked with **¿** will only be included when applicable.
580
- *
581
- * Properties marked with **¡** may be set automatically when applicable.
582
- * Currently that only applies to type handlers, which will be looked
583
- * up using `opts.handlers.for()` as a convenience shortcut.
584
- *
585
- * @prop {module:@lumjs/core/obj/cp.API} cp - API instance
586
- * @prop {object} opts - Current options; defaults to `cp.opts`
587
- * @prop {number} depth - Current recusion depth (`0` is top-level)
588
- * @prop {?object} prev - Previous context (if applicable)
589
- *
590
- * Will be `null` unless if `opts.recusive` is not `0` and the
591
- * context is for a nested operation (top-level will always be `null`).
592
- *
593
- * @prop {object} [target] Current target **¿**
594
- * @prop {object} [source] Current source **¿**
595
- * @prop {number} [ti] Current target index **¿**
596
- * @prop {number} [si] Current source index **¿**
597
- * @prop {module:@lumjs/core/obj/cp~Typ} [th] Handler for `target` **¡**
598
- * @prop {module:@lumjs/core/obj/cp~Typ} [sh] Handler for `source` **¡**
599
- *
600
- * @prop {(string|symbol)} [prop] Property being operated on **¿**
601
- * @prop {object} [td] Target descriptor for `prop` **¿**
602
- * @prop {object} [sd] Source descriptor for `prop` **¿**
603
- *
604
- * @alias module:@lumjs/core/obj/cp~Context
605
- */
606
- class CPContext
607
- {
608
- /**
609
- * Build a new Context object.
610
- *
611
- * Generally this is only called by the `cp.getContext()` API method.
612
- *
613
- * This will call `this.update(prev, data)` after setting
614
- * `this.cp` and `this.opts` to their default values.
615
- *
616
- * Lastly it sets `this.prev` overwriting any previous value.
617
- *
618
- * @param {module:@lumjs/core/obj/cp.API} api - API instance
619
- * @param {object} data - Initial data
620
- * @param {?module:@lumjs/core/obj/cp~Context} prev - Previous context
621
- */
622
- constructor(api, data, prev)
623
- {
624
- this.cp = api;
625
- this.opts = api.opts;
626
- this.depth = 0;
627
- this.update(prev, data);
628
- this.prev = prev;
629
- }
630
-
631
- /**
632
- * Update the context with new data
633
- *
634
- * Will automatically populate certain properties depending on
635
- * the data added.
636
- *
637
- * @param {...object} data - Properties to add to the context
638
- *
639
- * Nothing is
640
- *
641
- * @returns {object} `this`
642
- */
643
- update()
644
- {
645
- Object.assign(this, ...arguments);
646
-
647
- if (typeof this.opts.onUpdate === F)
648
- {
649
- this.opts.onUpdate(this, ...arguments);
650
- }
651
-
652
- if (this.target && !this.th)
653
- {
654
- this.th = this.opts.handlers.for(this.target);
655
- }
656
-
657
- if (this.source && !this.sh)
658
- {
659
- this.sh = this.opts.handlers.for(this.source);
660
- }
661
-
662
- return this;
663
- }
664
-
665
- /**
666
- * Set context options
667
- *
668
- * It makes a _new object_ composing the original `this.opts` and `changes`,
669
- * and sets that to `this.opts`. It does not change the original opts object.
670
- *
671
- * This is meant to be called from custom `onUpdate` handler functions,
672
- * and isn't actually used by anything in the library itself.
673
- *
674
- * @param {object} changes - Any option values you want to change.
675
- *
676
- * @returns {object} `this`
677
- */
678
- setOpts(changes)
679
- {
680
- this.opts = Object.assign({}, this.opts, changes);
681
- return this;
682
- }
683
-
684
- } // CPContext class
685
-
686
- /**
687
- * A class providing a declarative API for advanced `obj.cp` API calls.
688
- *
689
- * This is based off of the earlier `copyProps` declarative API,
690
- * and is meant to replace it entirely in future releases of this library.
691
- *
692
- * @alias module:@lumjs/core/obj/cp.API
693
- */
694
- class CPAPI
695
- {
696
- /**
697
- * Create a declarative `obj.cp` API instance.
698
- *
699
- * You generally would never call this directly, but use one of the
700
- * functions in `cp` instead, a few examples:
701
- *
702
- * ```js
703
- * // Copy properties recursively, allowing overwrite
704
- * cp.into(target).ow.deep.from(source);
705
- *
706
- * // Clone an object, including all (not just enumerable) properties
707
- * let cloned = cp.from(obj).all.clone();
708
- *
709
- * ```
710
- *
711
- * @param {(object|function)} [opts] Options
712
- *
713
- * If this is a `function` it will be used as the `opts.onUpdate` value.
714
- *
715
- * If this is a {@link module:@lumjs/core/obj/cp.HandlerSet} object,
716
- * it will be used as the `opts.handlers` value.
717
- *
718
- * @param {module:@lumjs/core/obj/cp.HandlerSet} [opts.handlers]
719
- * Specific type handlers to use with this API instance.
720
- *
721
- * If not specified, the default set will be used.
722
- *
723
- * @param {boolean} [opts.all=false] Include all properties?
724
- *
725
- * If `false` (default), we'll only use *enumerable* properties.
726
- * If `true` we'll include *all* property descriptors in the subject.
727
- *
728
- * @param {boolean} [opts.overwrite=false] Overwrite existing properties?
729
- *
730
- * @param {boolean} [opts.proto=false] Copy prototype?
731
- *
732
- * If `true`, the prototype on each target will be set to the
733
- * prototype of the first source. This is `false` by default.
734
- *
735
- * @param {number} [opts.recursive=0] Recurse into nested objects?
736
- *
737
- * - `0` → disables recursion entirely (default)
738
- * - `> 0` → specify depth recursion should be allowed
739
- * - `< 0` → unlimited recursion depth
740
- *
741
- * When recursion is enabled, we will cache nested objects that we've
742
- * already processed, so we don't get stuck in an infinite loop.
743
- *
744
- * @param {number} [opts.toggleDepth=-1] This is the recursion depth
745
- * that will be used when the `deep` toggle value is `true`.
746
- *
747
- * @param {function} [opts.onUpdate] A custom function
748
- */
749
- constructor(opts={})
750
- {
751
- if (typeof opts === F)
752
- {
753
- opts = {onUpdate: opts, handlers: TY.default};
754
- }
755
- else if (opts instanceof HandlerSet)
756
- {
757
- opts = {handlers: opts};
758
- }
759
- else if (!(opts.handlers instanceof HandlerSet))
760
- {
761
- opts.handlers = TY.default;
762
- }
763
-
764
- this.opts = Object.assign({}, DEFAULT_CPAPI_OPTS);
765
- this.set(opts);
766
-
767
- this.sources = [];
768
- this.targets = [];
769
-
770
- this.nextToggle = true;
771
- }
772
-
773
- /**
774
- * Get a context object to pass to type handler methods
775
- * @param {object} data - Initial data for the context object
776
- * @param {object} [prev] A previous context object (if applicable)
777
- * @returns {module:@lumjs/core/obj/cp~Context}
778
- */
779
- getContext(data, prev=null)
780
- {
781
- return new CPContext(this, data, prev);
782
- }
783
-
784
- /**
785
- * Use specific type handlers or a custom context onUpdate function.
786
- *
787
- * @param {(module:@lumjs/core/obj/cp.HandlerSet|function)} arg
788
- *
789
- * If this is a `function`, we will set `opts.onUpdate` using it.
790
- * Otherwise we will set `opts.handlers`.
791
- *
792
- * @returns {object} `this`
793
- * @throws {TypeError} If `arg` is invalid
794
- */
795
- use(arg)
796
- {
797
- const opt = typeof arg === F ? 'onUpdate' : 'handlers';
798
- return this.set({[opt]: arg}, true);
799
- }
800
-
801
- /**
802
- * The next *toggle accessor property* will be set to `false`.
803
- *
804
- * This only affects the very next toggle, after which the
805
- * toggle value will return to its default value of `true`.
806
- *
807
- * You need to explicitly use `not` before each toggle property
808
- * that you want to turn off instead of on.
809
- */
810
- get not()
811
- {
812
- this.nextToggle = false;
813
- return this;
814
- }
815
-
816
- /**
817
- * Private method that handles all *toggle properties*.
818
- * @private
819
- * @param {string} opt - Name of option to toggle
820
- * @param {*} [trueVal=true] Value to use if toggle is `true`
821
- * @param {*} [falseVal=false] Value to use if toggle is `false`
822
- * @returns {object} `this`
823
- */
824
- $toggle(opt, trueVal=true, falseVal=false)
825
- {
826
- this.opts[opt] = this.nextToggle ? trueVal : falseVal;
827
- this.nextToggle = true;
828
- return this;
829
- }
830
-
831
- /**
832
- * Toggle `opts.all` with boolean value when accessed
833
- */
834
- get all()
835
- {
836
- return this.$toggle('all');
837
- }
838
-
839
- /**
840
- * Toggle `opts.overwrite` with boolean value when accessed
841
- */
842
- get ow()
843
- {
844
- return this.$toggle('overwrite');
845
- }
846
-
847
- /**
848
- * Toggle `opts.recursive` when accessed
849
- *
850
- * Value to set depends on the toggle value:
851
- *
852
- * `true` → `opts.recursive = opts.toggleDepth`
853
- * `false` → `opts.recursive = 0`
854
- *
855
- */
856
- get deep()
857
- {
858
- const max = this.opts.toggleDepth;
859
- if (max === 0) console.error("toggleDepth is 0", {cp: this});
860
- return this.$toggle('recursive', max, 0);
861
- }
862
-
863
- /**
864
- * Set specified options
865
- * @param {object} opts - Options to set
866
- * @param {boolean} [fatal=false] Throw on invalid type?
867
- *
868
- * By default we report invalid option values to the console,
869
- * and simply skip setting the invalid option.
870
- *
871
- * If this is `true` any invalid option values will result in
872
- * an error being thrown, ending the entire `set()` process.
873
- *
874
- * @returns {object} `this`
875
- * @throws {TypeError} See `fatal` argument for details
876
- */
877
- set(opts, fatal=false)
878
- {
879
- if (isObj(opts))
880
- {
881
- for (const opt in opts)
882
- {
883
- if (opt in VALIDATION)
884
- {
885
- if (!VALIDATION[opt](opts[opt]))
886
- {
887
- const msg = 'invalid option value';
888
- const info = {opt, opts, cp: this};
889
- if (fatal)
890
- {
891
- console.error(info);
892
- throw new TypeError(msg);
893
- }
894
- else
895
- {
896
- console.error(msg, info);
897
- continue;
898
- }
899
- }
900
- }
901
- else if (opt in CPAPI_TOGGLE_OPTS)
902
- {
903
- if (!opts[opt])
904
- { // Negate the toggle.
905
- this.not;
906
- }
907
- // Now toggle it.
908
- this[opt];
909
- continue;
910
- }
911
-
912
- this.opts[opt] = opts[opt];
913
- }
914
- }
915
- else
916
- {
917
- console.error("invalid opts", {opts, cp: this});
918
- }
919
- return this;
920
- }
921
-
922
- /**
923
- * Specify the `targets` to copy properties into.
924
- *
925
- * @param {...object} [targets] The target objects
926
- *
927
- * If you don't specify any targets, then `this.targets`
928
- * will be cleared of any existing objects.
929
- *
930
- * If `this.sources` has objects in it, then we'll copy
931
- * those sources into each of the specified targets.
932
- *
933
- * If `this.sources` is empty, then this will set
934
- * `this.targets` to the specified value.
935
- *
936
- * @returns {object} `this`
937
- */
938
- into(...targets)
939
- {
940
- if (this.sources.length > 0 && targets.length > 0)
941
- {
942
- this.$runAll(this.sources, targets);
943
- }
944
- else
945
- {
946
- this.targets = targets;
947
- }
948
- return this;
949
- }
950
-
951
- /**
952
- * Specify the `sources` to copy properties from.
953
- *
954
- * @param {...object} [sources] The source objects.
955
- *
956
- * If you don't specify any sources, then `this.sources`
957
- * will be cleared of any existing objects.
958
- *
959
- * If `this.targets` has objects in it, then we'll copy
960
- * the specified sources into each of those targets.
961
- *
962
- * If `this.targets` is empty, then this will set
963
- * `this.sources` to the specified value.
964
- *
965
- * @returns {object} `this`
966
- */
967
- from(...sources)
968
- {
969
- if (this.targets.length > 0 && sources.length > 0)
970
- {
971
- this.$runAll(sources, this.targets);
972
- }
973
- else
974
- {
975
- this.sources = sources;
976
- }
977
- return this;
978
- }
979
-
980
- /**
981
- * Add additional subjects
982
- *
983
- * - If you started with `from()` this adds to the sources.
984
- * - If you started with `into()` this adds to the targets.
985
- *
986
- * @param {...object} subjects - More sources or targets to add
987
- * @returns {object} `this`
988
- */
989
- and(...subjects)
990
- {
991
- if (this.sources.length > 0)
992
- {
993
- this.sources.push(...subjects);
994
- }
995
- else if (this.targets.length > 0)
996
- {
997
- this.targets.push(...subjects);
998
- }
999
- else
1000
- {
1001
- console.error("No sources or targets set", {subjects, cp: this});
1002
- }
1003
- return this;
1004
- }
1005
-
1006
- /**
1007
- * See if we can use EZ mode.
1008
- *
1009
- * EZ-mode is a shortcut to use `cp()` or `cp.safe()`
1010
- * to perform copy operations if applicable.
1011
- */
1012
- get isEZ()
1013
- {
1014
- const o = this.opts;
1015
- return (!o.all && !o.recursive);
1016
- }
1017
-
1018
- /**
1019
- * Clone the properties of the sources into a new object.
1020
- *
1021
- * This will only with if `cp.into()` was used to create the
1022
- * API instance. It will throw an error if `cp.from()` was used.
1023
- *
1024
- * This gets the type handler for the first source, uses it
1025
- * to get a new object, then runs the copy operation with the
1026
- * new object as the sole target.
1027
- *
1028
- * @returns {object} A new object
1029
- * @throws {RangeError} If there are targets set instead of sources
1030
- */
1031
- clone()
1032
- {
1033
- if (this.targets.length > 0)
1034
- {
1035
- console.debug(this);
1036
- throw new RangeError("cannot clone when targets set");
1037
- }
1038
-
1039
- if (this.sources.length === 0)
1040
- {
1041
- console.debug(this);
1042
- throw new RangeError("cannot clone with no sources");
1043
- }
1044
-
1045
- // Get the type handler for the first source.
1046
- const sh = this.opts.handlers.for(this.sources[0]);
1047
- const target = sh.new();
1048
-
1049
- this.$runAll(this.sources, [target]);
1050
-
1051
- return target;
1052
- }
1053
-
1054
- /**
1055
- * Run a copy operation for the specified sources and targets.
1056
- *
1057
- * For _each target_, it will find the type handler, then do one of:
1058
- *
1059
- * - If `this.isEZ` is `true`, calls `cp()` or `cp.safe()` to perform
1060
- * the operation, and no further processing will be required.
1061
- *
1062
- * - If the type handler has a `copyAll()` method,
1063
- * that will be called, passing the `sources` array
1064
- * in its entirety.
1065
- *
1066
- * - If the type handler instead has a `copyOne()` method,
1067
- * that will be called once for each source.
1068
- *
1069
- * - If the handler has neither of those, then `this.$runOne()`
1070
- * will be called once for each source.
1071
- *
1072
- * @private
1073
- * @param {Array} sources
1074
- * @param {Array} targets
1075
- * @returns {void}
1076
- */
1077
- $runAll(sources, targets)
1078
- {
1079
- const ez = this.isEZ;
1080
- for (const ti in targets)
1081
- {
1082
- const target = targets[ti];
1083
-
1084
- if (ez)
1085
- { // EZ mode, use one of the simple functions.
1086
- const hdl = this.opts.handlers;
1087
- const fun = this.opts.overwrite ? cp : cp.safe;
1088
- fun(hdl, target, ...sources);
1089
- }
1090
- else
1091
- { // Advanced mode supports many more options.
1092
- const ctx = this.getContext({target,ti,cache: []});
1093
- //console.debug({target, ti, ctx});
1094
- const {th} = ctx;
1095
- if (typeof th.copyAll === F)
1096
- { // Type handler will handle all sources at once.
1097
- th.copyAll(ctx, sources);
1098
- }
1099
- else
1100
- { // One source at a time.
1101
- const isHandled = (typeof th.copyOne === F);
1102
- for (const si in sources)
1103
- {
1104
- const source = sources[si];
1105
- ctx.update({source,si});
1106
-
1107
- if (isHandled)
1108
- {
1109
- th.copyOne(ctx);
1110
- }
1111
- else
1112
- {
1113
- this.$runOne(ctx);
1114
- }
1115
- }
1116
- }
1117
- }
1118
- }
1119
- } // $runAll()
1120
-
1121
- /**
1122
- * Run a copy operation from a single source into a single target.
1123
- * This is called by `$runAll()`, and also calls itself recursively
1124
- * if `opts.recursive` is not set to `0` (its default value).
1125
- *
1126
- * @private
1127
- * @param {module:@lumjs/core/obj/cp~Context} ctx - Context;
1128
- * must have both `target` and `source` properties defined.
1129
- * @returns {void}
1130
- */
1131
- $runOne(rc)
1132
- {
1133
- const {target,source,cache} = rc;
1134
- const sprops = rc.sh.getProps(source, rc);
1135
- const tprops = rc.th.getProps(target, rc);
1136
-
1137
- for (const prop in sprops)
1138
- {
1139
- const sd = sprops[prop];
1140
- let td = tprops[prop];
1141
-
1142
- const pc = this.getContext({prop,sd}, rc);
1143
- const po = pc.opts;
1144
-
1145
- if ((po.recursive < 0 || (po.recursive > pc.depth))
1146
- && isObj(sd.value)
1147
- && !cache.includes(sd.value)
1148
- )
1149
- { // A nested object was found.
1150
- //console.debug("recursive mode", {sd,td});
1151
- cache.push(sd.value);
1152
- if (sd.value !== td?.value)
1153
- { // Not the same literal object.
1154
- pc.update({sh: null, source: sd.value});
1155
-
1156
- if (isObj(td))
1157
- { // Use the existing target descriptor
1158
- pc.update({td});
1159
- }
1160
- else
1161
- { // Create a new descriptor.
1162
- const value = pc.sh.new();
1163
- //console.debug({ph: pctx.sh ,value});
1164
- td = Object.assign({}, sd, {value});
1165
- pc.update({td});
1166
- def(target, prop, pc.td);
1167
- }
1168
-
1169
- //cache.push(td.value);
1170
- pc.update({th: null, target: td.value, depth: pc.depth+1});
1171
-
1172
- this.$runOne(pc);
1173
- }
1174
- }
1175
- else if (po.overwrite || td === undefined)
1176
- {
1177
- def(target, prop, pc.sd);
1178
- }
1179
- }
1180
-
1181
- if (rc.si === 0 && rc.opts.proto)
1182
- { // Prototype assignment only done with first source.
1183
- const sp = Object.getPrototypeOf(source);
1184
- const tp = Object.getPrototypeOf(target);
1185
- if (sp && sp !== tp)
1186
- { // Apply the source prototype to the target.
1187
- Object.setPrototypeOf(target, sp);
1188
- }
1189
- }
1190
-
1191
- } // $runOne()
1192
-
1193
- } // CPAPI class
1194
-
1195
- /**
1196
- * Create a new `cp` declarative API instance with the specified options.
1197
- * @param {(function|object)} opts - Passed to the constructor
1198
- * @returns {module:@lumjs/core/obj/cp.API} API instance
1199
- * @alias module:@lumjs/core/obj/cp.with
1200
- */
1201
- cp.with = function(opts)
1202
- {
1203
- return new CPAPI(opts);
1204
- }
1205
-
1206
- /**
1207
- * Create a new `cp` declarative API instance with default options,
1208
- * and the specified target objects.
1209
- * @param {...objects} targets - Target objects
1210
- * @returns {module:@lumjs/core/obj/cp.API} API instance
1211
- * @alias module:@lumjs/core/obj/cp.into
1212
- */
1213
- cp.into = function()
1214
- {
1215
- return (new CPAPI()).into(...arguments);
1216
- }
1217
-
1218
- /**
1219
- * Create a new `cp` declarative API instance with default options,
1220
- * and the specified source objects.
1221
- * @param {...objects} source - Source objects
1222
- * @returns {module:@lumjs/core/obj/cp.API} API instance
1223
- * @alias module:@lumjs/core/obj/cp.from
1224
- */
1225
- cp.from = function()
1226
- {
1227
- return (new CPAPI()).from(...arguments);
1228
- }
1229
-
1230
- /**
1231
- * A wrapper around `cp` specifically for cloning.
1232
- *
1233
- * @param {object} obj - Object to clone
1234
- *
1235
- * @param {?object} [opts] Options
1236
- *
1237
- * If this is an `object`, the clone call will be:
1238
- * `cp.with(opts).from(obj).ow.clone();`
1239
- *
1240
- * If this is `null` or `undefined`, the call will be:
1241
- * `cp(obj);`
1242
- *
1243
- * @returns {object} `obj`
1244
- * @alias module:@lumjs/core/obj/cp.clone
1245
- */
1246
- cp.clone = function(obj, opts)
1247
- {
1248
- if (isNil(opts))
1249
- {
1250
- return cp(obj);
1251
- }
1252
- else
1253
- {
1254
- return cp.with(opts).from(obj).ow.clone();
1255
- }
1256
- }
1257
-
1258
- /**
1259
- * Add a clone() method to an object.
1260
- *
1261
- * The method added is pretty simple with a very basic signature:
1262
- * `obj.clone(opts)`, and it calls `cp.clone(obj,opts);`
1263
- *
1264
- * @param {object} obj - Object to add method to
1265
- * @param {object} [spec] Optional spec
1266
- * @param {string} [spec.name='clone'] Method name to add;
1267
- * Default: `clone`
1268
- * @param {object} [spec.desc] Descriptor rules
1269
- * @param {?object} [spec.opts] Default options for method;
1270
- * Default is `null` so that simple cloning is the default.
1271
- *
1272
- * @returns {object} `obj`
1273
- * @alias module:@lumjs/core/obj/cp.addClone
1274
- */
1275
- function addClone(obj, spec={})
1276
- {
1277
- const name = spec.name ?? 'clone';
1278
- const desc = spec.desc ?? {};
1279
- const defs = spec.opts ?? null;
1280
-
1281
- desc.value = function(opts)
1282
- {
1283
- if (isObj(defs) && opts !== null)
1284
- {
1285
- opts = Object.assign({}, defs, opts);
1286
- }
1287
-
1288
- return cp.clone(obj, opts);
1289
- }
1290
-
1291
- return def(obj, name, desc);
1292
- }
1293
-
1294
- /**
1295
- * A singleton object offering cached `cp.API` instances.
1296
- * @alias module:@lumjs/core/obj/cp.cache
6
+ * @module @lumjs/core/obj/cp
1297
7
  */
1298
- cp.cache =
1299
- {
1300
- /**
1301
- * Return a `cp` instance with a single `target` object.
1302
- *
1303
- * Will cache the instance the first time, so future calls
1304
- * with the same `target` will return the same instance.
1305
- *
1306
- * @param {object} target
1307
- * @returns {module:@lumjs/core/obj/cp.API}
1308
- */
1309
- into(target)
1310
- {
1311
- if (this.intoCache === undefined)
1312
- this.intoCache = new Map();
1313
- const cache = this.intoCache;
1314
- if (cache.has(target))
1315
- {
1316
- return cache.get(target);
1317
- }
1318
- else
1319
- {
1320
- const api = cp.into(target);
1321
- cache.set(target, api);
1322
- return api;
1323
- }
1324
- },
1325
- /**
1326
- * Return a `cp` instance with a single `source` object.
1327
- *
1328
- * Will cache the instance the first time, so future calls
1329
- * with the same `target` will return the same instance.
1330
- *
1331
- * @param {object} source
1332
- * @returns {module:@lumjs/core/obj/cp.API}
1333
- */
1334
- from(source)
1335
- {
1336
- if (this.fromCache === undefined)
1337
- this.fromCache = new Map();
1338
- const cache = this.fromCache;
1339
- if (cache.has(source))
1340
- {
1341
- return cache.get(source);
1342
- }
1343
- else
1344
- {
1345
- const api = cp.from(source);
1346
- cache.set(source, api);
1347
- return api;
1348
- }
1349
- },
1350
- /**
1351
- * Clear the caches for `into` and `from`.
1352
- * @returns {object} `cp.cache`
1353
- */
1354
- clear()
1355
- {
1356
- if (this.intoCache)
1357
- this.intoCache.clear();
1358
- if (this.fromCache)
1359
- this.fromCache.clear();
1360
- return this;
1361
- },
1362
- } // cp.cache
1363
-
1364
- // Assign the rest of the named exports
1365
- Object.assign(cp,
1366
- {
1367
- addClone, TY, HandlerSet,
1368
- API: CPAPI,
1369
- Context: CPContext,
1370
- __cpArgs: cpArgs,
1371
- });
1372
-
1373
- // Export the module itself, including self reference.
1374
- module.exports = cp.cp = cp;
8
+ module.exports = require('@lumjs/cp/obj');