@lumjs/core 1.0.0-beta.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,183 @@
1
+ // Thanks to CJS `require()` rules, recursive dependencies are possible.
2
+ const unbound = require('./root').unbound;
3
+ const {F, B} = require('./js');
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);
9
+
10
+ /**
11
+ * A wrapper around `Object.defineProperty()`.
12
+ *
13
+ * This has a few features that makes adding properties a lot nicer.
14
+ * It replaces the `prop()` method from the old Lum.js v4.
15
+ *
16
+ * @param {(object|function)} obj - The object to add a property to.
17
+ * @param {?(string|boolean|object)} name - If a `string`, the property name.
18
+ * Property names may also be `symbol` values.
19
+ *
20
+ * If this is `null` or `undefined` then the `value` is ignored entirely,
21
+ * and instead a bound version of this function is created with the
22
+ * `obj` already passed as the first parameter, and any of the options from
23
+ * `opts` added to a new default set of options bound to the function.
24
+ * Can be useful if you need to add a lot of properties to the same object.
25
+ *
26
+ * If this is a `boolean`, then the same logic as if it was `null` or
27
+ * `undefined` will apply, except that an `enumerable` property with this
28
+ * value will also be added to the descriptors.
29
+ *
30
+ * If this is an `object`, we also ignore `value` entirely, as each of
31
+ * the keys of this object will be used as the name of a property,
32
+ * and the value associated with the key will be the value to assign it.
33
+ *
34
+ * @param {*} value - Used to determine the value of the property.
35
+ *
36
+ * If it is an a valid descriptor object (as per `doesDescriptor()`),
37
+ * it will be used as the descriptor. If it has no `configurable`
38
+ * property defined, one will be added, and will be set to `true`.
39
+ * This behaviour may be overridden by the `opts` parameter.
40
+ *
41
+ * If this and `opts` are both `function` values, then this will
42
+ * be used as a *getter*, and `opts` will be used a *setter*.
43
+ *
44
+ * Any other value passed here will be used as the `value` in a
45
+ * pre-canned descriptor, also with `configurable` set to `true`.
46
+ *
47
+ * @param {*} [opts] - A multi-purpose option.
48
+ *
49
+ * If this is an `object` then it is reserved for named options.
50
+ * The named options `configurable`, `enumerable`, and `writable`
51
+ * can be used to define default values to the descriptor properties
52
+ * of the same name.
53
+ *
54
+ * If `value` and this are both `function` values, then `value` will
55
+ * be used as a *getter* and this will be used as a *setter*.
56
+ *
57
+ * If `value` is a valid descriptor object, then setting this to `false`
58
+ * will disable the assumption that it is the descriptor to set.
59
+ * Setting this to `true` on will instruct the function to make a clone
60
+ * of the descriptor object before modifying any properties in it.
61
+ *
62
+ * This defaults to `undefined`, except if
63
+ *
64
+ * @returns {*} Normally the `obj` argument with new property added.
65
+ *
66
+ * The exception is if this is a bound copy of the function created
67
+ * using the syntax described in the `name` parameter documentation.
68
+ * In that case the return value is the bound copy itself.
69
+ * While that might seem strange, it allows for chaining in a way
70
+ * that otherwise would not be possible, take this example:
71
+ *
72
+ * ```
73
+ * def(myObj)('name', "Bob")('age', 42);
74
+ * ```
75
+ *
76
+ * @alias module:@lumjs/core/types.def
77
+ */
78
+ function def(obj, name, value, opts)
79
+ {
80
+ const isBound // Is this a 'bound' def function?
81
+ = !unbound(this, true, true)
82
+ && typeof this.bound === F;
83
+
84
+ // When we're finished, return this value.
85
+ const done = () =>
86
+ {
87
+ if (isBound)
88
+ { // Bound version, returns itself recursively.
89
+ return this.bound;
90
+ }
91
+ else
92
+ { // Not bound, or doesn't have a reference to itself.
93
+ return obj;
94
+ }
95
+ }
96
+
97
+ if (isBound && isNil(opts))
98
+ { // We'll use `this` as the options.
99
+ opts = this;
100
+ }
101
+
102
+ if (isObj(name))
103
+ { // Assume it's a map of {name: value} entries.
104
+ for (const key in name)
105
+ {
106
+ def(obj, key, name[key], opts);
107
+ }
108
+ // Okay, we're done now.
109
+ return done();
110
+ }
111
+ else if (isNil(name) || typeof name === B)
112
+ { // Create a fresh binding context for the bound function.
113
+ const bind = {};
114
+
115
+ if (isObj(opts))
116
+ { // Copy our existing options as defaults.
117
+ copy(bind, opts);
118
+ }
119
+
120
+ if (typeof name === B)
121
+ { // A boolean `name` overrides the enumerable option.
122
+ bind.enumerable = name;
123
+ }
124
+
125
+ // Create a bound function.
126
+ const bound = def.bind(bind, obj);
127
+ // Add a reference to the function in the binding context.
128
+ bind.bound = bound;
129
+ // And a reference to the binding options from the function.
130
+ bound.$this = bind;
131
+
132
+ return bound;
133
+ }
134
+ else if (!isProperty(name))
135
+ { // That's not valid.
136
+ throw new TypeError("Property name must be a string or a Symbol");
137
+ }
138
+
139
+ let desc;
140
+
141
+ if (opts !== false && doesDescriptor(value))
142
+ { // The value is a descriptor, let's use it.
143
+ desc = (opts === true) ? clone(value) : value;
144
+ }
145
+ else if (typeof value === F && typeof opts === F)
146
+ { // A getter and setter were both specified.
147
+ desc = {get: value, set: opts};
148
+ }
149
+ else
150
+ { // The value is just a value, so let's assign it.
151
+ desc = {value};
152
+ }
153
+
154
+ if (isObj(opts))
155
+ { // If opts is an object, let's look for some supported defaults.
156
+ const cd = (opt, defval) =>
157
+ {
158
+ if (typeof opts[opt] === B && typeof desc[opt] !== B)
159
+ { // Default descriptor option specified in `opts`
160
+ desc[opt] = opts[opt];
161
+ }
162
+ else if (typeof defval === B)
163
+ { // A fallback default.
164
+ desc[opt] = defval;
165
+ }
166
+ }
167
+
168
+ cd('configurable', true);
169
+ cd('enumerable');
170
+ if (desc.get === undefined && desc.set === undefined)
171
+ { // Only look for this one on data descriptors.
172
+ cd('writable');
173
+ }
174
+ } // if isObj(opts)
175
+
176
+ // Now after all that, let's actually define the property!
177
+ Object.defineProperty(obj, name, desc);
178
+
179
+ return done();
180
+
181
+ } // def()
182
+
183
+ module.exports = def;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Fundamental Types sub-module.
3
+ *
4
+ * As `@lumjs/core` is the foundation for all my JS libraries,
5
+ * this sub-module is the foundation for `@lumjs/core`.
6
+ * Everything else is built upon this.
7
+ *
8
+ * @module @lumjs/core/types
9
+ * @property {string} O - "object"
10
+ * @property {string} F - "function"
11
+ * @property {string} S - "string"
12
+ * @property {string} B - "binary"
13
+ * @property {string} N - "number"
14
+ * @property {string} U - "undefined"
15
+ * @property {string} SY - "symbol"
16
+ * @property {string} BI - "bigint"
17
+ */
18
+
19
+ // Constants representing core Javascript types.
20
+ const {O, F, S, B, N, U, SY, BI} = require('./js');
21
+
22
+ // Basic type check functions.
23
+ const
24
+ {
25
+ isObj, isComplex, isNil, notNil, isScalar, isArray, isTypedArray,
26
+ nonEmptyArray, isArguments, isProperty, doesDescriptor,
27
+ } = require('./basics');
28
+
29
+ // Root namespace helpers.
30
+ const {root, unbound} = require('./root');
31
+
32
+ // Advanced type checks.
33
+ const {isInstance, isType, isa} = require('./isa');
34
+
35
+ // Error-throwing type checks.
36
+ const {needObj, needType, needs} = require('./needs');
37
+
38
+ // A few standalone items.
39
+
40
+ const def = require('./def');
41
+ const TYPES = require('./typelist');
42
+ const stringify = require('./stringify');
43
+
44
+ // Okay, add all those to our exports.
45
+ // Further tests can be added by `TYPES.add()` later.
46
+ module.exports =
47
+ {
48
+ O, F, S, B, N, U, SY, BI, TYPES, root, unbound, def,
49
+ isObj, isComplex, isNil, notNil, isScalar, isArray, isTypedArray,
50
+ nonEmptyArray, isArguments, isProperty, doesDescriptor,
51
+ isInstance, isType, isa, needObj, needType, needs, stringify,
52
+ }
53
+
54
+ // Last but not least, this will be the module for TYPES.add()
55
+ def(TYPES, '$module', module);
@@ -0,0 +1,186 @@
1
+ const {O, F, S} = require('./js');
2
+ const {isObj} = require('./basics');
3
+ const TYPES = require('./typelist');
4
+
5
+ /**
6
+ * See if a value is an instance of a class.
7
+ * @param {*} v - The value we're testing.
8
+ * @param {function} f - The constructor/class we want.
9
+ * @param {boolean} [needProto=false] If true, the `v` must have a `prototype`.
10
+ * @returns {boolean}
11
+ * @alias module:@lumjs/core/types.isInstance
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
+ }
181
+
182
+ // None of the tests passed. We have failed.
183
+ return false;
184
+ }
185
+
186
+ exports.isa = isa;
@@ -0,0 +1,12 @@
1
+ // Fundamental core types.
2
+ const O='object', F='function', S='string', B='boolean', N='number',
3
+ U='undefined', SY='symbol', BI='bigint';
4
+
5
+ /**
6
+ * One or two character identifiers for the core JS type names.
7
+ *
8
+ * These are only the strings returned by the `typeof` operator.
9
+ * See the `TYPES` object (defined in `typelist.js`) for a list
10
+ * that includes special types and compound pseudo-types, etc.
11
+ */
12
+ module.exports = {O, F, S, B, N, U, SY, BI};
@@ -0,0 +1,117 @@
1
+ const {F, S, B} = require('./js');
2
+ const {isType, isa} = require('./isa');
3
+ const {isObj, isComplex} = require('./basics');
4
+
5
+ /**
6
+ * If a value is not an object, throw an error.
7
+ *
8
+ * @param {*} v - The value we're testing.
9
+ * @param {boolean} [allowFunc=false] - Also accept functions?
10
+ * @param {string} [msg] A custom error message.
11
+ * @throws {TypeError} If the type check failed.
12
+ * @alias module:@lumjs/core/types.needObj
13
+ */
14
+ function needObj (v, allowFunc=false, msg=null)
15
+ {
16
+ if (allowFunc && isComplex(v)) return;
17
+ if (isObj(v)) return;
18
+
19
+ if (typeof msg !== S)
20
+ { // Use a default message.
21
+ msg = "Invalid object";
22
+ if (allowFunc)
23
+ msg += " or function";
24
+ }
25
+ throw new TypeError(msg);
26
+ }
27
+
28
+ exports.needObj = needObj;
29
+
30
+ /**
31
+ * If a value is not a certain type, throw an error.
32
+ *
33
+ * @param {string} type - The type name as per `isType()`.
34
+ * @param {*} v - The value we're testing.
35
+ * @param {string} [msg] A custom error message.
36
+ * @throws {TypeError} If the type check failed.
37
+ * @alias module:@lumjs/core/types.needType
38
+ */
39
+ function needType (type, v, msg, unused)
40
+ {
41
+ if (!isType(type, v))
42
+ {
43
+ if (typeof msg === B)
44
+ {
45
+ console.warn("needType(): 'allowNull' is no longer supported");
46
+ if (typeof unused === S)
47
+ { // Compatibility with old code.
48
+ msg = unused;
49
+ }
50
+ }
51
+
52
+ if (typeof msg !== S)
53
+ { // Use a default message.
54
+ msg = `Invalid ${type} value`;
55
+ }
56
+
57
+ throw new TypeError(msg);
58
+ }
59
+ }
60
+
61
+ exports.needType = needType;
62
+
63
+ // Options parser for needs();
64
+ const NEEDS_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
+
72
+ /**
73
+ * A wrapper around `isa()` that will throw an error on failure.
74
+ *
75
+ * @param {*} v - The value we're testing.
76
+ * @param {...any} types - The types the value should be one of.
77
+ *
78
+ * In addition to the `types` supported by `isa()` this will also
79
+ * look for an `object` with a single property named `error`
80
+ * which can be either a `string` or any subclass of `Error`.
81
+ * If specified, it will override the error message that will be thrown.
82
+ *
83
+ * @throws {TypeError} If the type check failed.
84
+ * @throws {Error} If a custom error was specified.
85
+ * @alias module:@lumjs/core/types.needs
86
+ */
87
+ function needs(v, ...types)
88
+ {
89
+ let error;
90
+
91
+ function parser(type, v)
92
+ {
93
+ // Only process objects with a single `error` property.
94
+ if ('error' in type && Object.keys(type).length === 1)
95
+ {
96
+ if (typeof type.error === 'string')
97
+ { // An error message.
98
+ error = new TypeError(type.error);
99
+ }
100
+ else if (type.error instanceof Error)
101
+ { // An error object.
102
+ error = type.error;
103
+ }
104
+ }
105
+ }
106
+
107
+ if (!isa(v, {isa:{parsers: parser}}, ...types))
108
+ {
109
+ if (!(error instanceof Error))
110
+ {
111
+ error = new TypeError("value did not pass needs check");
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ exports.needs = needs;
@@ -0,0 +1,92 @@
1
+ const {U} = require('./js');
2
+ const {isNil,isArray} = require('./basics');
3
+
4
+ // «private»
5
+ function no_root()
6
+ {
7
+ throw new Error("Invalid JS environment, no root object found");
8
+ }
9
+
10
+ /**
11
+ * The global root object. Usually `globalThis` these days.
12
+ * @alias module:@lumjs/core/types.root
13
+ */
14
+ const root = typeof globalThis !== U ? globalThis
15
+ : typeof global !== U ? global
16
+ : typeof self !== U ? self
17
+ : typeof window !== U ? window
18
+ : no_root(); // Unlike the old way, we'll die if the environment is undetermined.
19
+
20
+ exports.root = root;
21
+
22
+ // A list of objects to be considered unbound globally.
23
+ const unboundObjects = [];
24
+
25
+ /**
26
+ * Pass `this` here to see if it is bound to an object.
27
+ *
28
+ * Always considers `null` and `undefined` as unbound.
29
+ *
30
+ * @param {*} whatIsThis - The `this` from any context.
31
+ * @param {boolean} [rootIsUnbound=true] The global root is unbound.
32
+ * @param {(boolean|Array)} [areUnbound=false] A list of unbound objects.
33
+ * If the is `true` we use an global list that can register special
34
+ * internal objects. Otherwise an `Array` of unbound objects may be used.
35
+ * @returns {boolean}
36
+ * @alias module:@lumjs/core/types.unbound
37
+ */
38
+ function unbound(whatIsThis, rootIsUnbound=true, areUnbound=false)
39
+ {
40
+ if (areUnbound === true)
41
+ { // If areUnbound is true, we use the unboundObjects
42
+ areUnbound = unboundObjects;
43
+ }
44
+
45
+ if (isNil(whatIsThis)) return true;
46
+ if (rootIsUnbound && whatIsThis === root) return true;
47
+ if (isArray(areUnbound) && areUnbound.includes(whatIsThis)) return true;
48
+
49
+ // Nothing considered unbound.
50
+ return false;
51
+ }
52
+
53
+ exports.unbound = unbound;
54
+
55
+ // Now that 'unbound' is exported, we can do some wibbly wobbly magic.
56
+ const def = require('./def');
57
+
58
+ /**
59
+ * Add an item to the unbound global objects list.
60
+ *
61
+ * @function
62
+ * @param {(object|function)} obj - The object to be considered unbound.
63
+ * @returns {boolean} Will be `false` if `obj` is already unbound.
64
+ * @throws {TypeError} If `obj` was neither an `object` nor a `function`.
65
+ * @alias module:@lumjs/core/types.unbound.add
66
+ */
67
+ def(unbound, 'add', function (obj)
68
+ {
69
+ needObj(obj, true);
70
+ if (unbound(obj, true, true))
71
+ { // Item is already unbound.
72
+ return false;
73
+ }
74
+ // Add to list and we're done.
75
+ unboundObjects.push(obj);
76
+ return true;
77
+ });
78
+
79
+ /**
80
+ * Remove an item from the unbound global objects list.
81
+ *
82
+ * @function
83
+ * @param {(object|function)} obj - The object to be removed.
84
+ * @returns {boolean} Will be `false` if the item was not in the list.
85
+ * @throws {TypeError} If `obj` was neither an `object` nor a `function`.
86
+ * @alias module:@lumjs/core/types.unbound.remove
87
+ */
88
+ def(unbound, 'remove', function(obj)
89
+ {
90
+ needObj(obj, true);
91
+ return (removeFromArray(unboundObjects, obj) > 0);
92
+ });