@nejs/basic-extensions 2.18.0 → 2.20.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,344 @@
1
+ import { Extension } from '@nejs/extension'
2
+ import { accessor, as, data, isDescriptor } from '../utils/index.js'
3
+
4
+ /**
5
+ * Creates an enumeration object with specified values and properties.
6
+ *
7
+ * @param {string} name - The name of the enumeration.
8
+ * @param {Array|any} values - The values to be included in the enumeration.
9
+ * If not an array, it will be converted into a single-element array.
10
+ * @param {Object|Map} [properties] - Additional properties to be added to
11
+ * the enumeration. Can be an object or a Map.
12
+ * @returns {Object} The constructed enumeration object.
13
+ *
14
+ * @example
15
+ * const colors = Enum('Colors', ['red', 'green', 'blue'])
16
+ * console.log(colors.red) // EnumValue object for 'red'
17
+ *
18
+ * @description
19
+ * The `Enum` function constructs an enumeration object with a given name,
20
+ * values, and optional properties. It supports primitive types, arrays,
21
+ * and objects as values. The function uses a combination of `Object.create`
22
+ * and `Proxy` to define and manage the properties of the enumeration.
23
+ *
24
+ * The enumeration object includes:
25
+ * - A `toString` method that returns the enumeration name.
26
+ * - A `Symbol.toStringTag` for identifying the object as an 'Enum'.
27
+ * - A `Symbol.for('Enum.name')` for storing the enumeration name.
28
+ *
29
+ * For array values, it creates a maker function that returns an
30
+ * `EnumValue` object with properties like `real`, `value`, `type`,
31
+ * `name`, and `compare`.
32
+ */
33
+ export function Enum(name, values, properties) {
34
+ const enumeration = Object.create({}, {
35
+ [Symbol.toStringTag]: accessor('Enum', false, true, false),
36
+ [Symbol.for('Enum.name')]: accessor(name, false, true, false),
37
+ [Symbol.for('Enum.valueKeys')]: data([], false, true, false),
38
+ [Symbol.for('nodejs.util.inspect.custom')]: data(
39
+ function(depth, options, inspect) {
40
+ const valueKeys = this[Symbol.for('Enum.valueKeys')] ?? []
41
+ let valueText = valueKeys
42
+ .map(key => `\x1b[1;2m${key}\x1b[22m`)
43
+ .join(', ')
44
+
45
+ if (valueText.length)
46
+ valueText = ` { ${valueText} }`
47
+
48
+ return `\x1b[1menum \x1b[22m${name}${valueText}`
49
+ }, false, true, false
50
+ ),
51
+ toString: data(function() {
52
+ return `Enum(${name})`
53
+ }, false, true, false)
54
+ })
55
+
56
+ if (!Array.isArray(values)) {
57
+ values = [values]
58
+ }
59
+
60
+ const asString = o => as.string(o, { description: true, stringTag: true })
61
+
62
+ /**
63
+ * A new base `EnumValue` type object. It contains enough custom symbols and
64
+ * identifiers to allow things like a `compare(to)` function to also work on
65
+ * each of the elements. Thing of this as the shared base functionality for
66
+ * each `Enum` element.
67
+ *
68
+ * @param {any} enumValue the value around which an `EnumValue` type is
69
+ * created.
70
+ * @returns an object defined by {@link Symbol.toStringTag} as well as some
71
+ * custom {@link Symbol} keys. The `node.js` custom inspect symbol is also
72
+ * defined for better REPL representation.
73
+ */
74
+ const makeEnumValue = (property, enumValue) => ({
75
+ toString: data(() => enumValue, false, true, false),
76
+
77
+ [Symbol.for('Enum.name')]: data(name, false, true, false),
78
+ [Symbol.for('Enum.is')]: data(true, false, false, false),
79
+ [Symbol.for('nodejs.util.inspect.custom')]: data(
80
+ function(depth, options, inspect) {
81
+ const _options = { ...(options || {}), colors: true }
82
+ const _skip = this.value === Symbol.for('Enum.NonAssociatedValue')
83
+ const _value = _skip
84
+ ? ''
85
+ : ` { value: ${inspect(this.value, _options) } }`;
86
+
87
+ return `${property}${_value}`
88
+ },
89
+ false, true, false
90
+ ),
91
+ [Symbol.toStringTag]: accessor('EnumValue', false, true, false),
92
+ [Symbol.for('compare')]: data(
93
+ function compareValue(to) {
94
+ const toObj = (to && typeof to === 'object') ? to : { real: to }
95
+ const kName = Symbol.for('Enum.name')
96
+
97
+ const hasAndIs = o =>
98
+ (Reflect.has(o, Symbol.for('Enum.is')) && o[Symbol.for('Enum.is')]);
99
+
100
+ const isLEnum = hasAndIs(this)
101
+ const isREnum = hasAndIs(toObj)
102
+
103
+ if (!isLEnum || !isREnum)
104
+ return false
105
+
106
+ const {real: lReal, value: lValue, name: lName, type: lType} = this
107
+ const {real: rReal, value: rValue, name: rName, type: rType} = toObj
108
+
109
+ return (
110
+ lName === rName && lType === rType &&
111
+ lReal === rReal && lValue === rValue
112
+ )
113
+ }, false, true, false),
114
+ [Symbol.toPrimitive]: data(
115
+ function EnumValueToPrimitive(hint) {
116
+ const original = this.real
117
+ const type = typeof original
118
+
119
+ switch (hint) {
120
+ case 'string':
121
+ if ('string' === type)
122
+ return original
123
+ else
124
+ return String(original)
125
+
126
+ case 'number':
127
+ if ('number' === type)
128
+ return original
129
+ else
130
+ return NaN
131
+
132
+ case 'bigint':
133
+ if ('bigint' === type)
134
+ return original
135
+ else
136
+ return NaN
137
+
138
+ default:
139
+ return original
140
+ }
141
+ },
142
+ false, true, false),
143
+ })
144
+
145
+ /**
146
+ * Given a value, determine how to represent it as both a key and a response
147
+ * or underlying original value. The method for this is dependent on the type
148
+ * of the value itself.
149
+ *
150
+ * @param {any} value the value to be converted
151
+ * @returns {[string, any]} an array where the first value is the transformed
152
+ * value as a key and the second element is the originally supplied value.
153
+ */
154
+ const fromPrimitive = (value) => {
155
+ let valueType = typeof value
156
+
157
+ switch (valueType) {
158
+ case 'string':
159
+ case 'number':
160
+ case 'bigint':
161
+ case 'boolean':
162
+ default:
163
+ return [String(value), value]
164
+
165
+ case 'symbol':
166
+ return [value.description, value]
167
+
168
+ case 'function':
169
+ return [value.name, value]
170
+
171
+ case 'object': {
172
+ const str = asString(value)
173
+ return [str, str]
174
+ }
175
+ }
176
+ }
177
+
178
+ // Determine the keys that the final proxy should be aware of when computing
179
+ // the enumeration value itself.
180
+ const kValueProps = ['real', 'value', 'type', 'name', 'compare', 'isEnum']
181
+ const kCustomPropKeys = []
182
+
183
+ // Capture and calculate any custom properties defined for each element
184
+ // of the enumeration. Each custom property is appended to `kCustomPropKeys`
185
+ const props = {}
186
+ if (properties) {
187
+ if (Array.isArray(properties)) {
188
+ const entries = properties.filter(e => Array.isArray(e) && e.length === 2)
189
+
190
+ if (entries.length)
191
+ properties = new Map(entries)
192
+ else
193
+ properties = new Map()
194
+ }
195
+ else if (typeof properties === 'object') {
196
+ properties = new Map(
197
+ Object.entries(Object.getOwnPropertyDescriptors(properties)))
198
+ }
199
+
200
+ if (properties instanceof Map) {
201
+ const applyPropertiesOf = (object, baseDescriptor) => {
202
+ const property = {
203
+ configurable: baseDescriptor?.configurable ?? true,
204
+ enumerable: baseDescriptor?.enumerable ?? true,
205
+ writable: baseDescriptor?.writable ?? true,
206
+ }
207
+
208
+ const descriptors = Object.getOwnPropertyDescriptors(object)
209
+ for (const [key, subvalue] of Object.entries(descriptors)) {
210
+ const stats = isDescriptor(subvalue, true)
211
+ const baseStats = isDescriptor(baseDescriptor, true)
212
+
213
+ if (stats.isAccessor && baseStats.isValid) {
214
+ props[key] = { ...subvalue, ...accessor.keys.from(baseDescriptor) }
215
+ }
216
+ else if (stats.isData && baseStats.isValid) {
217
+ props[key] = { ...subvalue, ...data.keys.from(baseDescriptor) }
218
+ }
219
+ }
220
+ }
221
+
222
+ let stats = {}
223
+
224
+ for (const [property, value] of properties.entries()) {
225
+ kCustomPropKeys.push(property)
226
+
227
+ if (isDescriptor(property)) {
228
+ if (typeof value === 'object') {
229
+ applyPropertiesOf(value, property)
230
+ continue
231
+ }
232
+ }
233
+
234
+ props[property] = value
235
+ }
236
+ }
237
+ }
238
+
239
+ for (const value of values) {
240
+ const valueType = Array.isArray(value) ? 'array' : typeof value
241
+
242
+ let property = undefined
243
+ let response = undefined
244
+ let makeNew = undefined
245
+ let wasArray = false
246
+ let elements = value
247
+
248
+ switch (valueType) {
249
+ case 'array':
250
+ if (value.length >= 1) {
251
+ wasArray = true;
252
+ ([property, response] = fromPrimitive(elements[0]))
253
+ }
254
+
255
+ default:
256
+ ([property, response] = fromPrimitive(value))
257
+ }
258
+
259
+ const maker = {
260
+ [property](initialValue) {
261
+ const storage = new Map()
262
+ const key = property
263
+ const realValue = accessor(response, false, { storage, key })
264
+
265
+ let _opts, associatedValue;
266
+
267
+ if (wasArray) {
268
+ _opts = { storage, key: key + '.associated' }
269
+ associatedValue = elements.length === 1
270
+ ? accessor(initialValue, true, _opts)
271
+ : accessor(elements?.[1], elements?.[2], _opts);
272
+ }
273
+ else
274
+ associatedValue = accessor(
275
+ Symbol.for('Enum.NonAssociatedValue'),
276
+ false, false, false)
277
+
278
+ const _prop = Object(asString(response))
279
+ const valueProps = [...kValueProps, ...kCustomPropKeys]
280
+ const enumResponse = Object.create(_prop, {
281
+ ...makeEnumValue(property, response),
282
+ ...props,
283
+ })
284
+
285
+ const proxy = new Proxy(_prop, {
286
+ get(target, _property, receiver) {
287
+ if (_property === 'real')
288
+ return realValue.get()
289
+
290
+ if (_property === 'value')
291
+ return associatedValue.get()
292
+
293
+ if (_property === 'type')
294
+ return name
295
+
296
+ if (_property === 'name')
297
+ return property
298
+
299
+ if (_property === 'compare')
300
+ return enumResponse[Symbol.for('compare')]
301
+
302
+ if (_property === 'isEnum')
303
+ return true
304
+
305
+ if (!valueProps.includes(_property))
306
+ return undefined
307
+ },
308
+ has(target, _property) {
309
+ return valueProps.includes(_property)
310
+ },
311
+ ownKeys(target) {
312
+ return valueProps
313
+ },
314
+ set(target, _property, value, receiver) {
315
+ if (_property === 'value' && wasArray)
316
+ return associatedValue.set(value)
317
+
318
+ return false
319
+ }
320
+ })
321
+
322
+ Object.setPrototypeOf(proxy, Object.getPrototypeOf(_prop))
323
+ Object.setPrototypeOf(enumResponse, proxy)
324
+
325
+ return enumResponse
326
+ }
327
+ }
328
+
329
+ enumeration[Symbol.for('Enum.valueKeys')].push(property)
330
+
331
+ const dataValue = wasArray ? maker[property] : maker[property]()
332
+ const baseProps = {
333
+ writable: false,
334
+ configurable: false,
335
+ enumerable: true,
336
+ }
337
+
338
+ Object.defineProperty(enumeration, property, data(dataValue, baseProps))
339
+ }
340
+
341
+ return enumeration
342
+ }
343
+
344
+ export const EnumExtension = new Extension(Enum)
@@ -9,6 +9,9 @@ export * from './deferred.js'
9
9
  import { Descriptor } from './descriptor.js'
10
10
  export * from './descriptor.js'
11
11
 
12
+ import { Enum } from './enum.js'
13
+ export * from './enum.js'
14
+
12
15
  import { Introspector } from './introspector.js'
13
16
  export * from './introspector.js'
14
17
 
@@ -41,6 +44,7 @@ export const NewClassesExtension = new Patch(globalThis, {
41
44
  AsyncIterator,
42
45
  Deferred,
43
46
  Descriptor,
47
+ Enum,
44
48
  Introspector,
45
49
  Iterable,
46
50
  Iterator,
package/src/index.js CHANGED
@@ -14,6 +14,7 @@ import { SymbolExtensions, SymbolPrototypeExtensions } from './symbol.extensions
14
14
 
15
15
  import { DeferredExtension } from './classes/deferred.js'
16
16
  import { DescriptorExtensions, Descriptor } from './classes/descriptor.js'
17
+ import { EnumExtension, Enum } from './classes/enum.js'
17
18
  import { IntrospectorExtensions } from './classes/introspector.js'
18
19
  import { IteratorExtensions, IterableExtensions } from './classes/iterable.js'
19
20
  import { ParamParserExtensions } from './classes/param.parser.js'
@@ -74,6 +75,7 @@ const Extensions = {
74
75
  [AsyncIteratorExtensions.key]: AsyncIteratorExtensions,
75
76
  [DeferredExtension.key]: DeferredExtension,
76
77
  [DescriptorExtensions.key]: DescriptorExtensions,
78
+ [EnumExtension.key]: EnumExtension,
77
79
  [IntrospectorExtensions.key]: IntrospectorExtensions,
78
80
  [IterableExtensions.key]: IterableExtensions,
79
81
  [IteratorExtensions.key]: IteratorExtensions,
@@ -93,7 +95,6 @@ for (const extension of Object.values(Extensions)) {
93
95
  Classes[fnOrClass.name] = fnOrClass
94
96
  }
95
97
 
96
-
97
98
  const Controls = {}
98
99
 
99
100
  Object.assign(Controls, {
@@ -335,7 +335,32 @@ export const DescriptorUtils = {
335
335
  }
336
336
 
337
337
  Object.defineProperty(accessor, 'keys', {
338
- get() { return ['get', 'set', 'configurable', 'enumerable'] },
338
+ get() { return Object.defineProperties(
339
+ ['get', 'set', 'configurable', 'enumerable'],
340
+ {
341
+ from: {
342
+ value: function extractKeysFrom(object) {
343
+ const response = {
344
+ get: undefined,
345
+ set: undefined,
346
+ configurable: undefined,
347
+ enumerable: undefined,
348
+ }
349
+
350
+ if (!object || !(object instanceof Object))
351
+ return response
352
+
353
+ for (const key of DescriptorUtils.accessor.keys) {
354
+ if (Reflect.has(object, key))
355
+ response[key] = object[key]
356
+ }
357
+ },
358
+ writable: false,
359
+ configurable: false,
360
+ enumerable: false
361
+ }
362
+ }
363
+ ) },
339
364
  configurable: true,
340
365
  enumerable: false,
341
366
  })
@@ -468,7 +493,32 @@ export const DescriptorUtils = {
468
493
  }
469
494
 
470
495
  Object.defineProperty(data, 'keys', {
471
- value: ['value', 'writable', 'configurable', 'enumerable'],
496
+ value: Object.defineProperties(
497
+ ['value', 'writable', 'configurable', 'enumerable'],
498
+ {
499
+ from: {
500
+ value: function extractKeysFrom(object) {
501
+ const response = {
502
+ value: undefined,
503
+ writable: undefined,
504
+ configurable: undefined,
505
+ enumerable: undefined,
506
+ }
507
+
508
+ if (!object || !(object instanceof Object))
509
+ return response
510
+
511
+ for (const key of DescriptorUtils.data.keys) {
512
+ if (Reflect.has(object, key))
513
+ response[key] = object[key]
514
+ }
515
+ },
516
+ writable: false,
517
+ configurable: false,
518
+ enumerable: false,
519
+ }
520
+ }
521
+ ),
472
522
  writable: false,
473
523
  configurable: true,
474
524
  enumerable: false
@@ -502,9 +552,6 @@ export const DescriptorUtils = {
502
552
  * stats block.
503
553
  */
504
554
  isDescriptor(value, returnStats = false, strict = true) {
505
- if (!value || typeof value !== 'object' || !(value instanceof Object))
506
- return false
507
-
508
555
  const areBools = (...props) => props.flat().every(
509
556
  prop => boolTypes.includes(typeof value[prop])
510
557
  );
@@ -532,6 +579,9 @@ export const DescriptorUtils = {
532
579
  isValid: false,
533
580
  }
534
581
 
582
+ if (!value || typeof value !== 'object' || !(value instanceof Object))
583
+ return returnStats ? stats : false;
584
+
535
585
  let score = 0
536
586
 
537
587
  if (value && typeof value === 'object') {