@lumjs/core 1.6.1 → 1.7.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/index.js CHANGED
@@ -51,8 +51,36 @@ has('lazy', {value: types.lazy});
51
51
 
52
52
  // The module builder itself.
53
53
  has('ModuleBuilder', {value: Builder});
54
- // And a shortcut function.
55
- has('buildModule', {value: function(m,o) { return Builder.build(m,o); }});
54
+
55
+ /**
56
+ * Return a set of functions for building a module.
57
+ *
58
+ * @name module:@lumjs/core.buildModule
59
+ * @function
60
+ * @see module:@lumjs/core/modulebuilder.build
61
+ */
62
+ has('buildModule',
63
+ {
64
+ value: function(m,o)
65
+ {
66
+ return Builder.build(m,o);
67
+ },
68
+ });
69
+
70
+ /**
71
+ * Create a new `ModuleBuilder` instance.
72
+ *
73
+ * @name module:@lumjs/core.newBuilder
74
+ * @function
75
+ * @see module:@lumjs/core/modulebuilder.new
76
+ */
77
+ has('newBuilder',
78
+ {
79
+ value: function(m, o)
80
+ {
81
+ return Builder.new(m,o);
82
+ }
83
+ });
56
84
 
57
85
  /**
58
86
  * Array utility functions «Lazy»
@@ -83,7 +111,7 @@ can('strings');
83
111
  can('flags');
84
112
 
85
113
  /**
86
- * Functions for manipulating objects
114
+ * Functions for manipulating objects «Lazy»
87
115
  * @name module:@lumjs/core.obj
88
116
  * @type {module:@lumjs/core/obj}
89
117
  */
@@ -137,12 +165,12 @@ from('objectid', 'randomNumber', 'InternalObjectId');
137
165
  from('meta', 'stacktrace', 'AbstractClass', 'Functions', 'NYI');
138
166
 
139
167
  /**
140
- * Create a magic *Enum* object
168
+ * Create a magic *Enum* object «Lazy»
141
169
  * @name module:@lumjs/core.Enum
142
170
  * @function
143
171
  * @see module:@lumjs/core/enum
144
172
  */
145
- can('Enum', {module: 'enum'});
173
+ can('Enum', true);
146
174
 
147
175
  /**
148
176
  * Make an object support the *Observable* API «Lazy»
@@ -1,18 +1,18 @@
1
1
 
2
- const {S,F,B,isObj,needObj,needType,def,lazy} = require('./types');
3
-
4
- function clone(obj)
2
+ const
5
3
  {
6
- const copy = {};
7
- for (const name in obj)
8
- {
9
- copy[name] = obj[name];
10
- }
11
- return copy;
12
- }
4
+ S,F,B,
5
+ isObj,needObj,needType,
6
+ def,lazy
7
+ } = require('./types');
8
+
9
+ const clone = require('./obj/copyall').duplicateOne;
13
10
 
14
11
  // Methods we want to export in the functional API.
15
- const BUILD_METHODS = ['has', 'can', 'from'];
12
+ const BUILD_METHODS = ['has', 'can', 'from', 'set'];
13
+
14
+ // Fallback locale
15
+ const DLOC = 'en-US';
16
16
 
17
17
  /**
18
18
  * A class to make building modules easier.
@@ -53,6 +53,16 @@ class ModuleBuilder
53
53
  * It's always possible to set the `conf.module` parameter manually as well,
54
54
  * which avoids the need to generate a separate parameter name.
55
55
  *
56
+ * @param {(string|boolean)} [opts.withLocale=false] Use locales?
57
+ *
58
+ * If this is a `string` it's the name of the locale (e.g. `en-US`).
59
+ *
60
+ * If this is `true` we'll use the default locale as determined by
61
+ * [getLocale()]{@link module:@lumjs/core/strings.getLocale}.
62
+ *
63
+ * If this is `false` we won't use locales (unless the `NESTED_CAMEL`
64
+ * mode is in use, in which case we'll use the default locale.)
65
+ *
56
66
  */
57
67
  constructor(targetModule, opts={})
58
68
  {
@@ -69,13 +79,28 @@ class ModuleBuilder
69
79
 
70
80
  this.nested = opts.nested ?? ModuleBuilder.NESTED_ERROR;
71
81
 
72
- this.strings = opts.strings;
73
- this.locale = opts.locale;
82
+ if (opts.withLocale)
83
+ { // Initialize the strings here.
84
+ this.withLocale(opts.locale);
85
+ }
74
86
  }
75
87
 
76
88
  // Get a descriptor for a module export.
77
89
  requireDescriptor(name, conf={})
78
90
  {
91
+ if (typeof conf === S)
92
+ { // A quick shortcut for setting module name.
93
+ conf = {module: conf};
94
+ }
95
+ else if (conf === true)
96
+ { // Use the lowercase name as the module.
97
+ conf = {module: this.$lc(name)};
98
+ }
99
+ else if (typeof conf.getter === F || typeof conf.setter === F)
100
+ { // It's an accessor-style descriptor already.
101
+ return conf;
102
+ }
103
+
79
104
  let value = conf.value;
80
105
 
81
106
  if (value === undefined)
@@ -100,6 +125,11 @@ class ModuleBuilder
100
125
  return {configurable, enumerable, writable, value};
101
126
  }
102
127
 
128
+ $lc(name)
129
+ {
130
+ return name.toLocaleLowerCase(this.locale ?? DLOC);
131
+ }
132
+
103
133
  // Normalize a property name.
104
134
  $normalizeProperty(name)
105
135
  {
@@ -132,16 +162,29 @@ class ModuleBuilder
132
162
  return name;
133
163
  }
134
164
 
135
- $normalizeWithCamelCase(names)
165
+ withLocale(loc)
136
166
  {
137
167
  if (this.strings === undefined)
138
- {
168
+ { // Load the strings library.
139
169
  this.strings = require('./strings');
140
170
  }
171
+
141
172
  if (this.locale === undefined)
142
- {
143
- this.locale = this.strings.getLocale();
173
+ { // Set the locale.
174
+ if (typeof loc === S)
175
+ { // Use an explicitly passed value.
176
+ this.locale = loc;
177
+ }
178
+ else
179
+ { // Use the default locale.
180
+ this.locale = this.strings.getLocale();
181
+ }
144
182
  }
183
+ }
184
+
185
+ $normalizeWithCamelCase(names)
186
+ {
187
+ this.withLocale();
145
188
  let name = names.shift().toLocaleLowerCase(this.locale);
146
189
  for (const path in names)
147
190
  {
@@ -155,7 +198,14 @@ class ModuleBuilder
155
198
  *
156
199
  * @param {string} name - The property name to export.
157
200
  *
158
- * @param {object} [conf] Additional export configuration options.
201
+ * @param {(object|string|true)} [conf] Additional export configuration options.
202
+ *
203
+ * If this is a `string` instead of an `object`, it's assumed to be the
204
+ * `conf.module` value.
205
+ *
206
+ * If this is `true` then the `conf` will be set with `module` being the
207
+ * lowercase version of `name`.
208
+ *
159
209
  * @param {string} [conf.module=`./${name}`] The module to `require()`.
160
210
  * @param {string} [conf.prop] If set, we want an exported property
161
211
  * of this name from the loaded module.
@@ -191,7 +241,7 @@ class ModuleBuilder
191
241
  * @param {object} [conf] Additional export configuration options.
192
242
  *
193
243
  * In addition to all the options supported by
194
- * [has()]{@link module:@lumjs/core/modules.has}
244
+ * [has()]{@link module:@lumjs/core/modules#has}
195
245
  * this also supports one further option.
196
246
  *
197
247
  * @param {object} [conf.lazy] Advanced options for the `lazy()` call.
@@ -268,6 +318,33 @@ class ModuleBuilder
268
318
  return this;
269
319
  }
270
320
 
321
+ /**
322
+ * Define an exported property in our module.
323
+ *
324
+ * This is literally just a wrapper around the
325
+ * [def()]{@link module:@lumjs/core/types.def} method,
326
+ * that passes `this.module.exports` as the `obj` parameter.
327
+ *
328
+ * @param {*} name - See `def()` for details.
329
+ * @param {*} value - See `def()` for details.
330
+ * @param {*} opts - See `def()` for details.
331
+ * @returns {(object|function)} What is returned may vary:
332
+ *
333
+ * If the `def()` returns a *bound* copy of itself, we'll return that
334
+ * bound function directly.
335
+ *
336
+ * In any other case, we return `this` ModuleBuilder instance.
337
+ */
338
+ set(name, value, opts)
339
+ {
340
+ const retval = def(this.module.exports, name, value, opts);
341
+ if (typeof retval === F && isObj(retval.$this) && retval.$this.bound === retval)
342
+ { // A bound copy was returned, we're going to return it directly.
343
+ return retval;
344
+ }
345
+ return this;
346
+ }
347
+
271
348
  /**
272
349
  * Create a functional API for the ModuleBuilder class.
273
350
  *
@@ -278,12 +355,14 @@ class ModuleBuilder
278
355
  * They each have a special `builder` property which is a reference
279
356
  * to the underlying `ModuleBuilder` instance.
280
357
  *
281
- * There's also a `builder` property in the exported function list.
358
+ * There's also a `builder` property in the exported function list,
359
+ * as well as a copy of the `def()` method for quick exporting.
282
360
  *
283
361
  * Example usage:
284
362
  *
285
363
  * ```js
286
- * const {has,can,from} = require('@lumjs/core').ModuleBuilder.build(module);
364
+ * const {has,can,from,set}
365
+ * = require('@lumjs/core').ModuleBuilder.build(module);
287
366
  * // or the shortcut: require('@lumjs/core).buildModule(module);
288
367
  *
289
368
  * // exports.foo = require('./foo');
@@ -301,7 +380,7 @@ class ModuleBuilder
301
380
  static build(targetModule, opts)
302
381
  {
303
382
  const builder = new this(targetModule, opts);
304
- const funcs = {builder};
383
+ const funcs = {builder, def};
305
384
  for (const name of BUILD_METHODS)
306
385
  {
307
386
  const func = function()
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * This is a 'dumb' copy method.
3
3
  *
4
- * It does no type checking, and has no qualms about overwriting properties.
5
- * You probably want something like `copyProps` instead.
4
+ * It only copies enumerable properties, does no type checking,
5
+ * and has no qualms about overwriting properties.
6
+ *
7
+ * Use `copyProps`, or `mergeNested` for more robust versions.
6
8
  *
7
9
  * @alias module:@lumjs/core/obj.copyAll
8
10
  */
@@ -17,5 +19,30 @@ function copyAll(target, ...sources)
17
19
  }
18
20
  return target;
19
21
  }
22
+ exports.copyAll = copyAll;
23
+
24
+ /**
25
+ * Make a copy of a single object using `copyAll`.
26
+ *
27
+ * Use `clone` for a more robust version.
28
+ *
29
+ * Alias: `copyAll.clone`
30
+ *
31
+ * @alias module:@lumjs/core/obj.duplicateOne
32
+ * @param {object} obj - The object to duplicate.
33
+ * @returns {object} A clone of the object.
34
+ */
35
+ exports.duplicateOne = copyAll.clone = obj => copyAll({}, obj);
20
36
 
21
- module.exports = copyAll;
37
+ /**
38
+ * Make a new object containing properties from other objects using `copyAll`.
39
+ *
40
+ * Use `copyProps.into({}).from(...sources)` for a more robust version.
41
+ *
42
+ * Alias: `copyAll.clone`
43
+ *
44
+ * @alias module:@lumjs/core/obj.duplicateOne
45
+ * @param {object} obj - The object to duplicate.
46
+ * @returns {object} A clone of the object.
47
+ */
48
+ exports.duplicateAll = copyAll.duplicate = () => copyAll({}, ...arguments);
@@ -1,6 +1,11 @@
1
1
 
2
2
  // Get some constants
3
- const {B,isObj,isComplex,isArray,def: defProp} = require('../types');
3
+ const {B,N,F,isObj,isArray,needObj,def} = require('../types');
4
+ const {duplicateOne: clone} = require('./copyall');
5
+
6
+ const RECURSE_NONE = 0;
7
+ const RECURSE_ALL = -1;
8
+ const RECURSE_LIST = -2;
4
9
 
5
10
  /**
6
11
  * Copy properties from one object to another.
@@ -8,61 +13,143 @@ const {B,isObj,isComplex,isArray,def: defProp} = require('../types');
8
13
  * @param {(object|function)} source - The object to copy properties from.
9
14
  * @param {(object|function)} target - The target to copy properties to.
10
15
  *
11
- * @param {object} [propOpts] Options for how to copy properties.
12
- * @param {boolean} [propOpts.default=true] Copy only enumerable properties.
13
- * @param {boolean} [propOpts.all=false] Copy ALL object properties.
14
- * @param {Array} [propOpts.props] A list of specific properties to copy.
15
- * @param {object} [propOpts.overrides] Descriptor overrides for properties.
16
- * @param {Array} [propOpts.exclude] A list of properties NOT to copy.
17
- * @param {*} [propOpts.overwrite=false] Overwrite existing properties.
18
- * If this is a `boolean` value, it will allow or disallow overwriting
19
- * of any and all properties in the target object.
16
+ * @param {object} [opts] Options for how to copy properties.
17
+ * @param {boolean} [opts.default=true] Copy only enumerable properties.
18
+ * @param {boolean} [opts.all=false] Copy ALL object properties.
19
+ * @param {Array} [opts.props] A list of specific properties to copy.
20
+ * @param {Array} [opts.exclude] A list of properties NOT to copy.
21
+ * @param {object} [opts.overrides] Descriptor overrides for properties.
22
+ *
23
+ * The object is considered a map, where each *key* is the name of the
24
+ * property, and the value should be an `object` containing any valid
25
+ * descriptor properties.
26
+ *
27
+ * If `opts.default` is explicitly set to `false` and `opts.overrides`
28
+ * is set, then not only will it be used as a list of overrides,
29
+ * but only the properties specified in it will be copied.
30
+ *
31
+ * @param {*} [opts.overwrite=false] Overwrite existing properties.
32
+ *
33
+ * If this is a `boolean` value, it will allow or disallow overwriting
34
+ * of any and all properties in the target object.
20
35
  *
21
- * If this is an object, it can be an Array of property names to allow
22
- * to be overwritten, or a map of property name to a boolean indicating
23
- * if that property can be overwritten or not.
36
+ * If this is an `object`, it can be an `Array` of property names to allow
37
+ * to be overwritten, or a *map* of property name to a `boolean` indicating
38
+ * if that property can be overwritten or not.
39
+ *
40
+ * Finally, if this is a `function` it'll be passed the property name and
41
+ * must return a boolean indicating if overwriting is allowed.
42
+ *
43
+ * @param {number} [opts.recursive=0] Enable recursive copying of objects.
44
+ *
45
+ * If it is `0` (also `copyProps.RECURSE_NONE`) then no recursion is done.
46
+ * In this case, the regular assignment rules (including `opts.overwrite`)
47
+ * will be used regardless of the property type.
48
+ * This is the default value.
49
+ *
50
+ * If this is *above zero*, it's the recursion depth for `object` properties.
51
+ *
52
+ * If this is *below zero*, then it should be one of the constant values:
53
+ *
54
+ * | Contant | Value | Description |
55
+ * | ------------------------ | ----- | ------------------------------------ |
56
+ * | `copyProps.RECURSE_ALL` | `-1` | Recurse to an *unlimited* depth. |
57
+ * | `copyProps.RECURSE_LIST` | `-2` | Recurse `opts.recurseOpts` props. |
58
+ *
59
+ * @param {object} [opts.recurseOpts] Options for recursive properties.
60
+ *
61
+ * If `opts.recursive` is not `0` then `opts.recurseOpts` can be a map of
62
+ * property names to further objects, which will be used as the `opts` for
63
+ * that property when calling `copyProps()` recursively.
64
+ *
65
+ * So you could have nested `opts.recurseOpts` values if required.
66
+ *
67
+ * The `recursive` property will automatically be added to the individual
68
+ * `recurseOpts`, automatically applying the correct value.
69
+ *
70
+ * The `opts.recurseOpts` option has an extra-special meaning if `opts.recurse`
71
+ * is set to `RECURSE_LIST`, as then *only* the properties with options defined
72
+ * in `opts.recurseOpts` will be recursed. The rest will simply be copied.
24
73
  *
25
74
  * @returns {object} The `target` object.
26
75
  * @alias module:@lumjs/core/obj.copyProps
27
76
  */
28
- function copyProps(source, target, propOpts)
77
+ function copyProps(source, target, opts={})
29
78
  {
30
79
  //console.debug("copyProps", source, target, propOpts);
31
- if (!isComplex(source) || !isComplex(target))
80
+ needObj(source, true, 'source must be an object or function');
81
+ needObj(target, true, 'target must be an object or function');
82
+ needObj(opts, false, 'opts must be an object');
83
+
84
+ const useDefaults = opts.default ?? true;
85
+ const overrides = opts.overrides ?? {};
86
+
87
+ let recursive;
88
+ if (typeof opts.recursive === N)
89
+ {
90
+ recursive = opts.recursive;
91
+ }
92
+ else if (typeof opts.recursive === B)
32
93
  {
33
- throw new TypeError("source and target both need to be objects");
94
+ recursive = opts.recursive ? RECURSE_ALL : RECURSE_NONE;
95
+ }
96
+ else
97
+ {
98
+ recursive = RECURSE_NONE;
34
99
  }
35
100
 
36
- if (!isObj(propOpts))
37
- propOpts = {default: true};
101
+ let recurseCache, recurseOpts;
102
+ if (recursive !== RECURSE_NONE)
103
+ {
104
+ recurseCache = opts.recurseCache ?? [];
105
+ recurseOpts = opts.recurseOpts ?? {};
106
+ }
38
107
 
39
- const defOverrides = propOpts.overrides ?? {};
40
- const defOverwrite = propOpts.overwrite ?? false;
108
+ let overwrites;
109
+ if (typeof opts.overwrite === F)
110
+ { // A custom function.
111
+ overwrites = opts.overwrite;
112
+ }
113
+ else if (isObj(opts.overwrite))
114
+ { // An object may be an array or a map.
115
+ if (isArray(opts.overwrite))
116
+ { // A flat array of properties to overwrite.
117
+ overwrites = prop => opts.overwrite.includes(prop);
118
+ }
119
+ else
120
+ { // A map of properties to overwrite.
121
+ overwrites = prop => opts.overwrite[prop] ?? false;
122
+ }
123
+ }
124
+ else
125
+ { // The only other values should be boolean.
126
+ overwrites = () => opts.overwrite ?? false;
127
+ }
41
128
 
42
- const exclude = isArray(propOpts.exclude) ? propOpts.exclude : null;
129
+ const exclude = isArray(opts.exclude) ? opts.exclude : null;
43
130
 
44
131
  let propDefs;
45
132
 
46
- if (propOpts.props && isArray(propOpts.props))
133
+ if (isArray(opts.props))
47
134
  {
48
- propDefs = propOpts.props;
135
+ propDefs = opts.props;
49
136
  }
50
- else if (propOpts.all)
137
+ else if (opts.all)
51
138
  {
52
139
  propDefs = Object.getOwnPropertyNames(source);
53
140
  }
54
- else if (propOpts.default)
141
+ else if (useDefaults)
55
142
  {
56
143
  propDefs = Object.keys(source);
57
144
  }
58
- else if (propOpts.overrides)
145
+ else
59
146
  {
60
- propDefs = Object.keys(propOpts.overrides);
147
+ propDefs = Object.keys(overrides);
61
148
  }
62
149
 
63
150
  if (!propDefs)
64
151
  {
65
- console.error("Could not determine properties to copy", propOpts);
152
+ console.error("Could not determine properties to copy", opts);
66
153
  return;
67
154
  }
68
155
 
@@ -71,34 +158,57 @@ function copyProps(source, target, propOpts)
71
158
  {
72
159
  //console.debug(" @prop:", prop);
73
160
  if (exclude && exclude.indexOf(prop) !== -1)
74
- continue; // Excluded property.
75
-
76
- let overwrite = false;
77
- if (typeof defOverwrite === B)
78
- {
79
- overwrite = defOverwrite;
80
- }
81
- else if (isObj(defOverwrite) && typeof defOverwrite[prop] === B)
82
- {
83
- overwrite = defOverwrite[prop];
161
+ { // Excluded property.
162
+ continue;
84
163
  }
85
164
 
86
- const def = Object.getOwnPropertyDescriptor(source, prop);
165
+ let desc = Object.getOwnPropertyDescriptor(source, prop);
87
166
  //console.debug(" @desc:", def);
88
- if (def === undefined) continue; // Invalid property.
167
+ if (desc === undefined)
168
+ { // A non-existent property.
169
+ continue; // Invalid property.
170
+ }
89
171
 
90
- if (isObj(defOverrides[prop]))
91
- {
92
- for (const key in defOverrides[prop])
172
+ if (isObj(overrides[prop]))
173
+ { // Overriding descriptor properties.
174
+ desc = clone(desc);
175
+ for (const key in overrides[prop])
93
176
  {
94
- const val = defOverrides[prop][key];
95
- def[key] = val;
177
+ const val = overrides[prop][key];
178
+ desc[key] = val;
96
179
  }
97
180
  }
98
181
 
99
- if (overwrite || target[prop] === undefined)
182
+ let overwrite = overwrites(prop);
183
+
184
+ if (recursive !== 0
185
+ && (recursive !== RECURSE_LIST || isObj(recurseOpts[prop]))
186
+ && isObj(desc.value)
187
+ && isObj(target[prop])
188
+ && !recurseCache.includes(desc.value)
189
+ && !recurseCache.includes(target[prop]))
190
+ { // Recursive mode is enabled, so we're going to go deeper.
191
+ recurseCache.push(decs.value);
192
+ if (desc.value !== target[prop])
193
+ { // They're not the same literal object already.
194
+ recurseCache.push(target[prop]);
195
+ const ropts
196
+ = isObj(recurseOpts[prop])
197
+ ? clone(recurseOpts[prop])
198
+ : {overwrite};
199
+ // Always set the cache.
200
+ ropts.recurseCache = recurseCache;
201
+ if (typeof ropts.recursive !== N)
202
+ { // Set the recursive option.
203
+ ropts.recursive = (recursive > 0) ? recursive - 1 : recursive;
204
+ }
205
+ // Okay, we're ready, let's recurse now!
206
+ copyProps(desc.value, target[prop], ropts);
207
+ }
208
+ }
209
+ else if (overwrite || target[prop] === undefined)
100
210
  { // Property doesn't already exist, let's add it.
101
- defProp(target, prop, def);
211
+ def(target, prop, desc);
102
212
  }
103
213
  }
104
214
 
@@ -107,4 +217,159 @@ function copyProps(source, target, propOpts)
107
217
  return target;
108
218
  } // copyProps()
109
219
 
220
+ def(copyProps, 'RECURSE_NONE', RECURSE_NONE);
221
+ def(copyProps, 'RECURSE_ALL', RECURSE_ALL);
222
+ def(copyProps, 'RECURSE_LIST', RECURSE_LIST);
223
+
224
+ /**
225
+ * A class providing a declarative `copyProps()` API,
226
+ * which makes it easy to copy one or more sources,
227
+ * into one or more targets.
228
+ *
229
+ * This class is not directly accessible, and instead is available
230
+ * via some special sub-methods of `copyProps`; examples:
231
+ *
232
+ * ```js
233
+ * // Get a copy of the `copyProps` function.
234
+ * const cp = require('@lumjs/core').obj.copyProps;
235
+ *
236
+ * // Starting with the target:
237
+ * cp.into(targetObj).given({all: true}).from(source1, source2);
238
+ *
239
+ * // Starting with the sources:
240
+ * cp.from(sourceObj).given({exclude: ['dontCopy']}).into(target1, target2);
241
+ *
242
+ * // Starting with the options:
243
+ * cp.given({recursive: cp.RECURSE_ALL}).from(source1, source2).into(target1, target2);
244
+ *
245
+ *
246
+ * ```
247
+ *
248
+ * @alias module:@lumjs/core/obj~CopyProps
249
+ */
250
+ class $CopyProps
251
+ {
252
+ constructor(opts={})
253
+ {
254
+ this.opts = opts;
255
+ this.sources = [];
256
+ this.targets = [];
257
+ }
258
+
259
+ /**
260
+ * Set a single option.
261
+ * @param {string} name - The option name
262
+ * @param {*} value - The option value
263
+ * @returns {object} `this`
264
+ */
265
+ set(name, value)
266
+ {
267
+ this.opts[name] = value;
268
+ return this;
269
+ }
270
+
271
+ /**
272
+ * Set all of the options.
273
+ *
274
+ * This replaces the currently set options entirely.
275
+ *
276
+ * @param {object} opts - The options to set.
277
+ * @returns {object} `this`
278
+ */
279
+ given(opts)
280
+ {
281
+ needObj(opts);
282
+ this.opts = opts;
283
+ return this;
284
+ }
285
+
286
+ /**
287
+ * Specify the `targets` to copy properties into.
288
+ *
289
+ * @param {...object} [targets] The target objects
290
+ *
291
+ * If `this.sources` has objects in it already,
292
+ * then we'll run `copyProps()` for each of the
293
+ * sources into each of the `targets`.
294
+ *
295
+ * If `this.sources` is empty, then this will set
296
+ * `this.target` to the specified value.
297
+ *
298
+ * You can specify no sources at all to clear the
299
+ * currently set `this.targets` value.
300
+ *
301
+ * @returns {object} `this`
302
+ */
303
+ into(...targets)
304
+ {
305
+ if (this.sources.length > 0 && targets.length > 0)
306
+ {
307
+ this.$run(this.sources, targets);
308
+ }
309
+ else
310
+ {
311
+ this.targets = targets;
312
+ }
313
+ return this;
314
+ }
315
+
316
+ /**
317
+ * Specify the `sources` to copy properties from.
318
+ *
319
+ * @param {...object} [sources] The source objects.
320
+ *
321
+ * If `this.targets` has objects in it already,
322
+ * then we'll run `copyProps()` for each of the
323
+ * `sources` into each of the targets.
324
+ *
325
+ * If `this.targets` is empty, then this will set
326
+ * `this.sources` to the specified value.
327
+ *
328
+ * You can specify no sources at all to clear the
329
+ * currently set `this.sources` value.
330
+ *
331
+ * @returns {object} `this`
332
+ */
333
+ from(...sources)
334
+ {
335
+ if (this.targets.length > 0 && sources.length > 0)
336
+ {
337
+ this.$run(sources, this.targets);
338
+ }
339
+ else
340
+ {
341
+ this.sources = sources;
342
+ }
343
+ return this;
344
+ }
345
+
346
+ // Protected method doesn't need to be documented.
347
+ $run(sources, targets)
348
+ {
349
+ for (const source of sources)
350
+ {
351
+ for (const target of targets)
352
+ {
353
+ copyProps(source, target, this.opts);
354
+ }
355
+ }
356
+ }
357
+
358
+ }
359
+
360
+ copyProps.given = function(opts)
361
+ {
362
+ return new $CopyProps(opts);
363
+ }
364
+
365
+ copyProps.into = function(...targets)
366
+ {
367
+ return ((new $CopyProps()).into(...targets));
368
+ }
369
+
370
+ copyProps.from = function(...sources)
371
+ {
372
+ return ((new $CopyProps()).from(...sources));
373
+ }
374
+
110
375
  module.exports = copyProps;
package/lib/obj/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module @lumjs/core/obj
4
4
  */
5
5
 
6
- const copyAll = require('./copyall');
6
+ const {copyAll,duplicateOne,duplicateAll} = require('./copyall');
7
7
  const copyProps = require('./copyprops');
8
8
  const {CLONE,clone,addClone,cloneIfLocked} = require('./clone');
9
9
  const {lock,addLock} = require('./lock');
@@ -21,5 +21,5 @@ module.exports =
21
21
  CLONE, clone, addClone, cloneIfLocked, lock, addLock,
22
22
  mergeNested, syncNested, copyProps, copyAll, ns,
23
23
  getObjectPath, setObjectPath, getNamespace, setNamespace,
24
- getProperty,
24
+ getProperty, duplicateAll, duplicateOne,
25
25
  }
package/lib/obj/merge.js CHANGED
@@ -1,48 +1,56 @@
1
1
  // Import required bits here.
2
- const {B, isObj} = require('../types');
2
+ const {B, N, isObj} = require('../types');
3
+ const copyProps = require('./copyprops');
4
+
5
+ // A shortcut for the recursive option.
6
+ const recursive = copyProps.RECURSE_ALL;
3
7
 
4
8
  /**
5
9
  * Merge two objects recursively.
6
10
  *
7
- * This is currently suseptible to circular reference infinite loops,
8
- * but given what it's designed for, I'm not too worried about that.
11
+ * This used to be a standalone function, but was poorly designed.
12
+ * It's now a wrapper around the
13
+ * [copyProps()]{@link module:@lumjs/core/obj.copyProps} method.
9
14
  *
10
15
  * @param {object} source - The source object we're copying from.
11
16
  * @param {object} target - The target object we're copying into.
12
17
  *
13
- * @param {object} [opts] Options that change the behaviour.
14
- * @param {boolean} [opts.overwrite=true] Allow overwriting.
15
- * Unlike `copy` which does not allow overwriting by default,
16
- * this method does. It's not designed for the same kind of objects
17
- * as `copy`, but more for JSON structured configuration files.
18
- *
19
- * As this is currently the only option, passing the boolean
20
- * as the `opts` argument directly will set this option.
18
+ * @param {(object|boolean)} [opts] Options for `copyProps()`
19
+ *
20
+ * If `opts.recursive` is not a `number`, it'll be set to
21
+ * `copyProps.RECURSE_ALL` to enable recursion with no
22
+ * depth limits.
23
+ *
24
+ * Also, if `opts.overwrite` is not explicitly set, it will
25
+ * be set as `true`, a different default value than `copyProps()`.
26
+ *
27
+ * For backwards compatibility, if this specified as a `boolean`
28
+ * value instead of an object, it'll be assumed to be value
29
+ * for the `opts.overwrite` option.
21
30
  *
22
31
  * @returns {object} The `target` object.
23
32
  * @alias module:@lumjs/core/obj.mergeNested
24
33
  */
25
- function mergeNested(source, target, opts={})
26
- {
27
- if (typeof opts === B)
28
- opts = {overwrite: opts};
29
-
30
- const overwrite = opts.overwrite ?? true;
31
-
32
- for (const prop in source)
33
- {
34
- if (isObj(source[prop]) && isObj(to[prop]))
35
- { // Nested objects, recurse deeper.
36
- mergeNested(source[prop], target[prop], opts);
37
- }
38
- else if (overwrite || target[prop] === undefined)
39
- { // Not tested objects, do a simple assignment.
40
- target[prop] = source[prop];
41
- }
42
- }
43
-
44
- return target;
45
- }
34
+ function mergeNested(source, target, opts={})
35
+ {
36
+ if (typeof opts === B)
37
+ { // Boolean overwrite option was specified.
38
+ opts = {overwrite: opts, recursive};
39
+ }
40
+ else if (!isObj(opts))
41
+ { // Wasn't an object, use default values.
42
+ opts = {overwrite: true, recursive};
43
+ }
44
+ else
45
+ { // If recursive or overwrite aren't set, set them.
46
+ if (opts.recursive === undefined)
47
+ opts.recursive = recursive;
48
+ if (opts.overwrite === undefined)
49
+ opts.overwrite = true;
50
+ }
51
+
52
+ return copyProps(source, target, opts);
53
+ }
46
54
 
47
55
  exports.mergeNested = mergeNested;
48
56
 
package/lib/types/def.js CHANGED
@@ -2,10 +2,7 @@
2
2
  const unbound = require('./root').unbound;
3
3
  const {F, B} = require('./js');
4
4
  const {isObj, isNil, isProperty, doesDescriptor} = require('./basics');
5
- const copy = require('../obj/copyall');
6
-
7
- // A really really cheap version of clone().
8
- const clone = obj => copy({}, obj);
5
+ const {copy, duplicateOne: clone} = require('../obj/copyall');
9
6
 
10
7
  /**
11
8
  * A wrapper around `Object.defineProperty()`.
package/lib/types/isa.js CHANGED
@@ -10,177 +10,174 @@ const TYPES = require('./typelist');
10
10
  * @returns {boolean}
11
11
  * @alias module:@lumjs/core/types.isInstance
12
12
  */
13
- function isInstance(v, f, needProto=false)
14
- {
15
- if (!isObj(v)) return false; // Not an object.
16
- if (needProto && (typeof v.prototype !== O || v.prototype === null))
17
- { // Has no prototype.
18
- return false;
19
- }
20
-
21
- if (typeof f !== F || !(v instanceof f)) return false;
22
-
23
- // Everything passed.
24
- return true;
25
- }
26
-
27
- exports.isInstance = isInstance;
28
-
29
- /**
30
- * A smarter `typeof` function.
31
- *
32
- * @param {string} type - The type we're checking for.
33
- *
34
- * This supports all the same type names as `typeof` plus any of
35
- * the properties defined in the `TYPES` object.
36
- *
37
- * One other thing, while `typeof` reports `null` as being an `object`,
38
- * this function does not count `null` as a valid `object`.
39
- *
40
- * @param {*} v - The value we're testing.
41
- * @returns {boolean} If the value was of the desired type.
42
- * @alias module:@lumjs/core/types.isType
43
- */
44
- function isType(type, v)
45
- {
46
- if (typeof type !== S || !TYPES.list.includes(type))
47
- {
48
- throw new TypeError(`Invalid type ${JSON.stringify(type)} specified`);
49
- }
50
-
51
- if (typeof TYPES.tests[type] === F)
52
- { // A type-specific test.
53
- return TYPES.tests[type](v);
54
- }
55
- else
56
- { // No type-specific tests.
57
- return (typeof v === type);
58
- }
59
- }
60
-
61
- exports.isType = isType;
62
-
63
- // Default options parser.
64
- const DEFAULT_ISA_PARSER = function(type, v)
65
- { // `this` is the options object itself.
66
- if (typeof type.is === F)
67
- { // We assume `is()` methods are the type check.
68
- if (type.is(v)) return true;
69
- }
70
-
71
- if (typeof type.isa === O)
72
- { // Process our known options.
73
- const opts = type.isa;
74
-
75
- if (typeof opts.process === F)
76
- { // We'll pass the current `opts` as well as the `v` to this method.
77
- // It doesn't return anything on its own, but can assign further tests,
78
- // which by default only apply to `object`
79
- opts.process(this, v);
80
- }
81
-
82
- if (typeof opts.parsers === F)
83
- { // Assign another parser.
84
- this.parsers.push(opts.parsers);
85
- }
86
-
87
- if (typeof opts.test === F)
88
- { // A custom test, will be passed `v` and the current `opts`.
89
- if (opts.test(v, this))
90
- { // Mark this as having passed.
91
- return true;
92
- }
93
- }
94
-
95
- // Okay, now anything else gets set.
96
- const RESERVED = ['parsers','process','test'];
97
- for (const opt in opts)
98
- {
99
- if (RESERVED.includes(opt)) continue; // Skip it.
100
- this[opt] = opts[opt];
101
- }
102
-
103
- }
104
-
105
- // We should almost always return false.
106
- return false;
107
- }
108
-
109
- /**
110
- * Is value a certain type, or an instance of a certain class.
111
- *
112
- * @param {*} v - The value we're testing.
113
- * @param {...any} types - The types the value should be one of.
114
- *
115
- * For each of the `types`, if it is a `string` we test with `isType()`,
116
- * if it is a `function` we test with `isInstance()`.
117
- *
118
- * If it is an `object` and has an `is()` method, use that as the test.
119
- *
120
- * If it is an `object` it will be passed to the current options parsers.
121
- * The default options parser looks for an `object` property called `isa`,
122
- * which supports the following child properties:
123
- *
124
- * - `needProto: boolean`, Change the `needProto` option for `isInstance()`
125
- * - `parsers: function`, Add another options parser function.
126
- * - `process: function`, A one-time set-up function.
127
- * - `test: function`, Pass the `v` to this test and return `true` if it passes.
128
- * - Anything else will be set as an option.
129
- *
130
- * Any other type value will only match if `v === type`
131
- *
132
- * @returns {boolean} Was the value one of the desired types?
133
- * @alias module:@lumjs/core/types.isa
134
- */
135
- function isa(v, ...types)
136
- {
137
- // A special options object.
138
- const opts =
139
- {
140
- needProto: false,
141
- parsers:
142
- [
143
- DEFAULT_ISA_PARSER,
144
- ],
145
- process(type, v)
146
- {
147
- for (const parser of this.parsers)
148
- {
149
- if (typeof parser === F)
150
- {
151
- if (parser.call(this, type, v) === true)
152
- { // Returning true means a custom test passed.
153
- return true;
154
- }
155
- }
156
- }
157
- return false;
158
- },
159
- }
160
-
161
- for (const type of types)
162
- {
163
- // First a quick test for absolute equality.
164
- if (v === type) return true;
165
-
166
- // With that out of the way, let's go!
167
- if (typeof type === S)
168
- { // A string is passed to isType()
169
- if (isType(type, v)) return true;
170
- }
171
- else if (typeof type === F)
172
- { // A function is passed to isInstance()
173
- if (isInstance(v, type, opts.needProto)) return true;
174
- }
175
- else if (isObj(type))
176
- { // Objects can be additional tests, or options.
177
- if (opts.process(type, v)) return true;
178
- }
179
-
180
- }
13
+ function isInstance(v, f, needProto=false)
14
+ {
15
+ if (!isObj(v)) return false; // Not an object.
16
+ if (needProto && (typeof v.prototype !== O || v.prototype === null))
17
+ { // Has no prototype.
18
+ return false;
19
+ }
20
+
21
+ if (typeof f !== F || !(v instanceof f)) return false;
22
+
23
+ // Everything passed.
24
+ return true;
25
+ }
181
26
 
182
- // None of the tests passed. We have failed.
183
- return false;
184
- }
27
+ exports.isInstance = isInstance;
28
+
29
+ /**
30
+ * A smarter `typeof` function.
31
+ *
32
+ * @param {string} type - The type we're checking for.
33
+ *
34
+ * This supports all the same type names as `typeof` plus any of
35
+ * the properties defined in the `TYPES` object.
36
+ *
37
+ * One other thing, while `typeof` reports `null` as being an `object`,
38
+ * this function does not count `null` as a valid `object`.
39
+ *
40
+ * @param {*} v - The value we're testing.
41
+ * @returns {boolean} If the value was of the desired type.
42
+ * @alias module:@lumjs/core/types.isType
43
+ */
44
+ function isType(type, v)
45
+ {
46
+ if (typeof type !== S || !TYPES.list.includes(type))
47
+ {
48
+ throw new TypeError(`Invalid type ${JSON.stringify(type)} specified`);
49
+ }
50
+
51
+ if (typeof TYPES.tests[type] === F)
52
+ { // A type-specific test.
53
+ return TYPES.tests[type](v);
54
+ }
55
+ else
56
+ { // No type-specific tests.
57
+ return (typeof v === type);
58
+ }
59
+ }
60
+
61
+ exports.isType = isType;
62
+
63
+ // Default options parser.
64
+ const DEFAULT_ISA_PARSER = function(type, v)
65
+ { // `this` is the options object itself.
66
+ if (typeof type.is === F)
67
+ { // We assume `is()` methods are the type check.
68
+ if (type.is(v)) return true;
69
+ }
70
+
71
+ if (typeof type.isa === O)
72
+ { // Process our known options.
73
+ const opts = type.isa;
74
+
75
+ if (typeof opts.process === F)
76
+ { // Run a method to extend the options further.
77
+ opts.process(this, v, type);
78
+ }
79
+
80
+ if (typeof opts.parsers === F)
81
+ { // Assign another parser.
82
+ this.parsers.push(opts.parsers);
83
+ }
84
+
85
+ if (typeof opts.test === F)
86
+ { // A custom test, will be passed `v` and the current `opts`.
87
+ if (opts.test(v, this))
88
+ { // Mark this as having passed.
89
+ return true;
90
+ }
91
+ }
92
+
93
+ // Okay, now anything else gets set.
94
+ const RESERVED = ['parsers','process','test'];
95
+ for (const opt in opts)
96
+ {
97
+ if (RESERVED.includes(opt)) continue; // Skip it.
98
+ this[opt] = opts[opt];
99
+ }
100
+
101
+ }
102
+
103
+ // We should almost always return false.
104
+ return false;
105
+ }
106
+
107
+ // Process further options
108
+ function processOptions(type, v)
109
+ {
110
+ for (const parser of this.parsers)
111
+ {
112
+ if (typeof parser === F)
113
+ {
114
+ if (parser.call(this, type, v) === true)
115
+ { // Returning true means a custom test passed.
116
+ return true;
117
+ }
118
+ }
119
+ }
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * Is value a certain type, or an instance of a certain class.
125
+ *
126
+ * @param {*} v - The value we're testing.
127
+ * @param {...any} types - The types the value should be one of.
128
+ *
129
+ * For each of the `types`, if it is a `string` we test with `isType()`,
130
+ * if it is a `function` we test with `isInstance()`.
131
+ *
132
+ * If it is an `object` and has an `is()` method, use that as the test.
133
+ *
134
+ * If it is an `object` it will be passed to the current options parsers.
135
+ * The default options parser looks for an `object` property called `isa`,
136
+ * which supports the following child properties:
137
+ *
138
+ * - `needProto: boolean`, Change the `needProto` option for `isInstance()`
139
+ * - `parsers: function`, Add another options parser function.
140
+ * - `process: function`, A one-time set-up function.
141
+ * - `test: function`, Pass the `v` to this test and return `true` if it passes.
142
+ * - Anything else will be set as an option.
143
+ *
144
+ * Any other type value will only match if `v === type`
145
+ *
146
+ * @returns {boolean} Was the value one of the desired types?
147
+ * @alias module:@lumjs/core/types.isa
148
+ */
149
+ function isa(v, ...types)
150
+ {
151
+ // A special options object.
152
+ const opts =
153
+ {
154
+ needProto: false,
155
+ parsers: [DEFAULT_ISA_PARSER],
156
+ process: processOptions,
157
+ }
158
+
159
+ for (const type of types)
160
+ {
161
+ // First a quick test for absolute equality.
162
+ if (v === type) return true;
163
+
164
+ // With that out of the way, let's go!
165
+ if (typeof type === S)
166
+ { // A string is passed to isType()
167
+ if (isType(type, v)) return true;
168
+ }
169
+ else if (typeof type === F)
170
+ { // A function is passed to isInstance()
171
+ if (isInstance(v, type, opts.needProto)) return true;
172
+ }
173
+ else if (isObj(type))
174
+ { // Objects can be additional tests, or options.
175
+ if (opts.process(type, v)) return true;
176
+ }
177
+ }
178
+
179
+ // None of the tests passed. We have failed.
180
+ return false;
181
+ }
185
182
 
186
- exports.isa = isa;
183
+ exports.isa = isa;
@@ -6,23 +6,42 @@ const {isObj, isComplex} = require('./basics');
6
6
  * If a value is not an object, throw an error.
7
7
  *
8
8
  * @param {*} v - The value we're testing.
9
- * @param {boolean} [allowFunc=false] - Also accept functions?
9
+ * @param {(boolean|string)} [allowFunc=false] - Also accept `function`?
10
+ *
11
+ * By default this function uses `isObj()` to perform the
12
+ * test. If `allowFunc` is `true` then it'll use `isComplex()` instead.
13
+ *
14
+ * If this is a `string` then it'll be assigned to the `msg`
15
+ * parameter, and `allowFunc` will be `true` if the word
16
+ * `function` is found in the message, or `false` otherwise.
17
+ *
10
18
  * @param {string} [msg] A custom error message.
19
+ *
20
+ * If not specified, a generic one will be used.
21
+ *
11
22
  * @throws {TypeError} If the type check failed.
12
23
  * @alias module:@lumjs/core/types.needObj
13
24
  */
14
25
  function needObj (v, allowFunc=false, msg=null)
15
26
  {
16
- if (allowFunc && isComplex(v)) return;
17
- if (isObj(v)) return;
27
+ if (typeof allowFunc === S)
28
+ { // A message was passed.
29
+ msg = allowFunc;
30
+ allowFunc = msg.toLowerCase().includes(F);
31
+ }
18
32
 
19
- if (typeof msg !== S)
20
- { // Use a default message.
21
- msg = "Invalid object";
22
- if (allowFunc)
23
- msg += " or function";
33
+ const ok = allowFunc ? isComplex(v) : isObj(v);
34
+
35
+ if (!ok)
36
+ { // Did not pass the test.
37
+ if (typeof msg !== S)
38
+ { // Use a default message.
39
+ msg = "Invalid object";
40
+ if (allowFunc)
41
+ msg += " or function";
42
+ }
43
+ throw new TypeError(msg);
24
44
  }
25
- throw new TypeError(msg);
26
45
  }
27
46
 
28
47
  exports.needObj = needObj;
@@ -101,6 +120,7 @@ function needs(v, ...types)
101
120
  {
102
121
  error = new TypeError("value did not pass needs check");
103
122
  }
123
+ console.error("needs()", v, types);
104
124
  throw error;
105
125
  }
106
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumjs/core",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
4
4
  "main": "lib/index.js",
5
5
  "exports":
6
6
  {