@nejs/basic-extensions 2.17.0 → 2.19.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,339 @@
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(Object.entries(properties))
197
+ }
198
+
199
+ if (properties instanceof Map) {
200
+ const applyPropertiesOf = (object, baseDescriptor) => {
201
+ const property = {
202
+ configurable: baseDescriptor?.configurable ?? true,
203
+ enumerable: baseDescriptor?.enumerable ?? true,
204
+ writable: baseDescriptor?.writable ?? true,
205
+ }
206
+
207
+ for (const [key, subvalue] of Object.entries(object)) {
208
+ if ((stats = isDescriptor(subvalue)).isValid) {
209
+ if (stats.isAccessor || stats.isData)
210
+ props[key] = subvalue
211
+ }
212
+ else
213
+ props[key] = data(subvalue, property, false, true, false)
214
+ }
215
+ }
216
+
217
+ let stats = {}
218
+
219
+ for (const [property, value] of properties.entries()) {
220
+ kCustomPropKeys.push(property)
221
+
222
+ if (isDescriptor(property)) {
223
+ if (typeof value === 'object') {
224
+ applyPropertiesOf(value, property)
225
+ continue
226
+ }
227
+ }
228
+
229
+ props[property] = data(value)
230
+ }
231
+ }
232
+ }
233
+
234
+ for (const value of values) {
235
+ const valueType = Array.isArray(value) ? 'array' : typeof value
236
+
237
+ let property = undefined
238
+ let response = undefined
239
+ let makeNew = undefined
240
+ let wasArray = false
241
+ let elements = value
242
+
243
+ switch (valueType) {
244
+ case 'array':
245
+ if (value.length >= 1) {
246
+ wasArray = true;
247
+ ([property, response] = fromPrimitive(elements[0]))
248
+ }
249
+
250
+ default:
251
+ ([property, response] = fromPrimitive(value))
252
+ }
253
+
254
+ const maker = {
255
+ [property](initialValue) {
256
+ const storage = new Map()
257
+ const key = property
258
+ const realValue = accessor(response, false, { storage, key })
259
+
260
+ let _opts, associatedValue;
261
+
262
+ if (wasArray) {
263
+ _opts = { storage, key: key + '.associated' }
264
+ associatedValue = elements.length === 1
265
+ ? accessor(initialValue, true, _opts)
266
+ : accessor(elements?.[1], elements?.[2], _opts);
267
+ }
268
+ else
269
+ associatedValue = accessor(
270
+ Symbol.for('Enum.NonAssociatedValue'),
271
+ false, false, false)
272
+
273
+ const _prop = Object(asString(response))
274
+ const valueProps = [...kValueProps, ...kCustomPropKeys]
275
+ const enumResponse = Object.create(_prop, {
276
+ ...makeEnumValue(property, response),
277
+ ...props,
278
+ })
279
+
280
+ const proxy = new Proxy(_prop, {
281
+ get(target, _property, receiver) {
282
+ if (_property === 'real')
283
+ return realValue.get()
284
+
285
+ if (_property === 'value')
286
+ return associatedValue.get()
287
+
288
+ if (_property === 'type')
289
+ return name
290
+
291
+ if (_property === 'name')
292
+ return property
293
+
294
+ if (_property === 'compare')
295
+ return enumResponse[Symbol.for('compare')]
296
+
297
+ if (_property === 'isEnum')
298
+ return true
299
+
300
+ if (!valueProps.includes(_property))
301
+ return undefined
302
+ },
303
+ has(target, _property) {
304
+ return valueProps.includes(_property)
305
+ },
306
+ ownKeys(target) {
307
+ return valueProps
308
+ },
309
+ set(target, _property, value, receiver) {
310
+ if (_property === 'value' && wasArray)
311
+ return associatedValue.set(value)
312
+
313
+ return false
314
+ }
315
+ })
316
+
317
+ Object.setPrototypeOf(proxy, Object.getPrototypeOf(_prop))
318
+ Object.setPrototypeOf(enumResponse, proxy)
319
+
320
+ return enumResponse
321
+ }
322
+ }
323
+
324
+ enumeration[Symbol.for('Enum.valueKeys')].push(property)
325
+
326
+ const dataValue = wasArray ? maker[property] : maker[property]()
327
+ const baseProps = {
328
+ writable: false,
329
+ configurable: false,
330
+ enumerable: true,
331
+ }
332
+
333
+ Object.defineProperty(enumeration, property, data(dataValue, baseProps))
334
+ }
335
+
336
+ return enumeration
337
+ }
338
+
339
+ 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, {