]> Lady’s Gitweb - Pisces/blob - object.js
Add LazyLoader class
[Pisces] / object.js
1 // ♓🌟 Piscēs ∷ object.js
2 // ====================================================================
3 //
4 // Copyright © 2022 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import { bind, call } from "./function.js";
11 import { toPrimitive, type } from "./value.js";
12
13 /**
14 * An object whose properties are lazy‐loaded from the methods on the
15 * own properties of the provided object.
16 *
17 * This is useful when you are looking to reference properties on
18 * objects which, due to module dependency graphs, cannot be guaranteed
19 * to have been initialized yet.
20 *
21 * The resulting properties will have the same attributes (regarding
22 * configurability, enumerability, and writability) as the
23 * corresponding properties on the methods object. If a property is
24 * marked as writable, the method will never be called if it is set
25 * before it is gotten. By necessity, the resulting properties are all
26 * configurable before they are accessed for the first time.
27 *
28 * Methods will be called with the resulting object as their this
29 * value.
30 *
31 * LazyLoader objects have the same prototype as the passed methods
32 * object.
33 */
34 export class LazyLoader extends null {
35 /** Constructs a new LazyLoader object. */
36 constructor(loadMethods) {
37 const result = objectCreate(getPrototype(loadMethods));
38 const methodKeys = getOwnPropertyKeys(loadMethods);
39 for (let index = 0; index < methodKeys.length; ++index) {
40 const methodKey = methodKeys[index];
41 const { configurable, enumerable, writable } =
42 getOwnPropertyDescriptor(loadMethods, methodKey);
43 defineOwnProperty(result, methodKey, {
44 configurable: true,
45 enumerable,
46 get: () => {
47 const value = call(loadMethods[methodKey], result, []);
48 defineOwnProperty(result, methodKey, {
49 configurable,
50 enumerable,
51 value,
52 writable,
53 });
54 return value;
55 },
56 set: writable
57 ? ($) =>
58 defineOwnProperty(result, methodKey, {
59 configurable,
60 enumerable,
61 value: $,
62 writable,
63 })
64 : void {},
65 });
66 }
67 return result;
68 }
69 }
70
71 /**
72 * A property descriptor object.
73 *
74 * Actually constructing a property descriptor object using this class
75 * is only necessary if you need strict guarantees about the types of
76 * its properties; the resulting object is proxied to ensure the types
77 * match what one would expect from composing FromPropertyDescriptor
78 * and ToPropertyDescriptor in the Ecmascript specification.
79 *
80 * Otherwise, the instance properties and methods are generic.
81 */
82 export const { PropertyDescriptor } = (() => {
83 class PropertyDescriptor extends null {
84 /**
85 * Constructs a new property descriptor object from the provided
86 * object.
87 *
88 * The resulting object is proxied to enforce types (for example,
89 * its `enumerable` property, if defined, will always be a
90 * boolean).
91 */
92 //deno-lint-ignore constructor-super
93 constructor(O) {
94 if (type(O) !== "object") {
95 // The provided value is not an object.
96 throw new TypeError(
97 "Piscēs: Cannot convert primitive to property descriptor.",
98 );
99 } else {
100 // The provided value is an object.
101 const desc = objectCreate(propertyDescriptorPrototype);
102 if ("enumerable" in O) {
103 // An enumerable property is specified.
104 desc.enumerable = !!O.enumerable;
105 } else {
106 // An enumerable property is not specified.
107 /* do nothing */
108 }
109 if ("configurable" in O) {
110 // A configurable property is specified.
111 desc.configurable = !!O.configurable;
112 } else {
113 // A configurable property is not specified.
114 /* do nothing */
115 }
116 if ("value" in O) {
117 // A value property is specified.
118 desc.value = O.value;
119 } else {
120 // A value property is not specified.
121 /* do nothing */
122 }
123 if ("writable" in O) {
124 // A writable property is specified.
125 desc.writable = !!O.writable;
126 } else {
127 // A writable property is not specified.
128 /* do nothing */
129 }
130 if ("get" in O) {
131 // A get property is specified.
132 const getter = O.get;
133 if (getter !== undefined && typeof getter !== "function") {
134 // The getter is not callable.
135 throw new TypeError("Piscēs: Getters must be callable.");
136 } else {
137 // The getter is callable.
138 desc.get = getter;
139 }
140 } else {
141 // A get property is not specified.
142 /* do nothing */
143 }
144 if ("set" in O) {
145 // A set property is specified.
146 const setter = O.set;
147 if (setter !== undefined && typeof setter !== "function") {
148 // The setter is not callable.
149 throw new TypeError("Piscēs: Setters must be callable.");
150 } else {
151 // The setter is callable.
152 desc.set = setter;
153 }
154 } else {
155 // A set property is not specified.
156 /* do nothing */
157 }
158 if (
159 ("get" in desc || "set" in desc) &&
160 ("value" in desc || "writable" in desc)
161 ) {
162 // Both accessor and data attributes have been defined.
163 throw new TypeError(
164 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
165 );
166 } else {
167 // The property descriptor is valid.
168 return new Proxy(desc, propertyDescriptorProxyHandler);
169 }
170 }
171 }
172
173 /**
174 * Completes this property descriptor by setting missing values to
175 * their defaults.
176 *
177 * This method modifies this object and returns undefined.
178 */
179 complete() {
180 if (this !== undefined && !("get" in this || "set" in this)) {
181 // This is a generic or data descriptor.
182 if (!("value" in this)) {
183 // `value` is not defined on this.
184 this.value = undefined;
185 } else {
186 // `value` is already defined on this.
187 /* do nothing */
188 }
189 if (!("writable" in this)) {
190 // `writable` is not defined on this.
191 this.writable = false;
192 } else {
193 // `writable` is already defined on this.
194 /* do nothing */
195 }
196 } else {
197 // This is not a generic or data descriptor.
198 if (!("get" in this)) {
199 // `get` is not defined on this.
200 this.get = undefined;
201 } else {
202 // `get` is already defined on this.
203 /* do nothing */
204 }
205 if (!("set" in this)) {
206 // `set` is not defined on this.
207 this.set = undefined;
208 } else {
209 // `set` is already defined on this.
210 /* do nothing */
211 }
212 }
213 if (!("enumerable" in this)) {
214 // `enumerable` is not defined on this.
215 this.enumerable = false;
216 } else {
217 // `enumerable` is already defined on this.
218 /* do nothing */
219 }
220 if (!("configurable" in this)) {
221 // `configurable` is not defined on this.
222 this.configurable = false;
223 } else {
224 // `configurable` is already defined on this.
225 /* do nothing */
226 }
227 }
228
229 /** Gets whether this is an accessor descrtiptor. */
230 get isAccessorDescriptor() {
231 return this !== undefined && ("get" in this || "set" in this);
232 }
233
234 /** Gets whether this is a data descrtiptor. */
235 get isDataDescriptor() {
236 return this !== undefined &&
237 ("value" in this || "writable" in this);
238 }
239
240 /** Gets whether this is a fully‐populated property descriptor. */
241 get isFullyPopulated() {
242 return this !== undefined &&
243 ("value" in this && "writable" in this ||
244 "get" in this && "set" in this) &&
245 "enumerable" in this && "configurable" in this;
246 }
247
248 /**
249 * Gets whether this is a generic (not accessor or data)
250 * descrtiptor.
251 */
252 get isGenericDescriptor() {
253 return this !== undefined &&
254 !("get" in this || "set" in this || "value" in this ||
255 "writable" in this);
256 }
257 }
258
259 const coercePropertyDescriptorValue = (P, V) => {
260 switch (P) {
261 case "configurable":
262 case "enumerable":
263 case "writable":
264 return !!V;
265 case "value":
266 return V;
267 case "get":
268 if (V !== undefined && typeof V !== "function") {
269 throw new TypeError(
270 "Piscēs: Getters must be callable.",
271 );
272 } else {
273 return V;
274 }
275 case "set":
276 if (V !== undefined && typeof V !== "function") {
277 throw new TypeError(
278 "Piscēs: Setters must be callable.",
279 );
280 } else {
281 return V;
282 }
283 default:
284 return V;
285 }
286 };
287
288 const {
289 prototype: propertyDescriptorPrototype,
290 } = PropertyDescriptor;
291
292 const propertyDescriptorProxyHandler = Object.assign(
293 Object.create(null),
294 {
295 defineProperty(O, P, Desc) {
296 if (
297 P === "configurable" || P === "enumerable" ||
298 P === "writable" || P === "value" ||
299 P === "get" || P === "set"
300 ) {
301 // P is a property descriptor attribute.
302 const desc = new PropertyDescriptor(Desc);
303 if ("get" in desc || "set" in desc) {
304 // Desc is an accessor property descriptor.
305 throw new TypeError(
306 "Piscēs: Property descriptor attributes must be data properties.",
307 );
308 } else if ("value" in desc || !(P in O)) {
309 // Desc has a value or P does not already exist on O.
310 desc.value = coercePropertyDescriptorValue(P, desc.value);
311 } else {
312 // Desc is not an accessor property descriptor and has no
313 // value.
314 /* do nothing */
315 }
316 const isAccessorDescriptor = "get" === P || "set" === P ||
317 "get" in O || "set" in O;
318 const isDataDescriptor = "value" === P || "writable" === P ||
319 "value" in O || "writable" in O;
320 if (isAccessorDescriptor && isDataDescriptor) {
321 // Both accessor and data attributes will be present on O
322 // after defining P.
323 throw new TypeError(
324 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
325 );
326 } else {
327 // P can be safely defined on O.
328 return defineOwnProperty(O, P, desc);
329 }
330 } else {
331 // P is not a property descriptor attribute.
332 return defineOwnProperty(O, P, Desc);
333 }
334 },
335 set(O, P, V, Receiver) {
336 if (
337 P === "configurable" || P === "enumerable" ||
338 P === "writable" || P === "value" ||
339 P === "get" || P === "set"
340 ) {
341 // P is a property descriptor attribute.
342 const newValue = coercePropertyDescriptorValue(P, V);
343 const isAccessorDescriptor = "get" === P || "set" === P ||
344 "get" in O || "set" in O;
345 const isDataDescriptor = "value" === P || "writable" === P ||
346 "value" in O || "writable" in O;
347 if (isAccessorDescriptor && isDataDescriptor) {
348 // Both accessor and data attributes will be present on O
349 // after defining P.
350 throw new TypeError(
351 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
352 );
353 } else {
354 // P can be safely defined on O.
355 //
356 // ☡ Receiver will be the *proxied* object, so passing it
357 // through to setPropertyValue here would produce an
358 // infinite loop.
359 //
360 // ☡ This has implications on objects with a proxied
361 // PropertyDescriptor in their prototype.
362 return setPropertyValue(O, P, newValue, O);
363 }
364 } else {
365 return setPropertyValue(O, P, V, Receiver);
366 }
367 },
368 setPrototypeOf(O, V) {
369 if (V !== propertyDescriptorPrototype) {
370 // V is not the property descriptor prototype.
371 return false;
372 } else {
373 // V is the property descriptor prototype.
374 return setPrototype(O, V);
375 }
376 },
377 },
378 );
379
380 return { PropertyDescriptor };
381 })();
382
383 export const {
384 /**
385 * Defines own properties on the provided object using the
386 * descriptors on the enumerable own properties of the provided
387 * additional objects.
388 *
389 * ※ This differs from Object.defineProperties in that it can take
390 * multiple source objects.
391 */
392 defineOwnProperties,
393 } = (() => {
394 const { defineProperties } = Object;
395 const { forEach: arrayForEach } = Array.prototype;
396 return {
397 defineOwnProperties: (O, ...sources) => {
398 call(
399 arrayForEach,
400 sources,
401 [(source) => defineProperties(O, source)],
402 );
403 return O;
404 },
405 };
406 })();
407
408 export const {
409 /**
410 * Defines an own property on the provided object on the provided
411 * property key using the provided property descriptor.
412 *
413 * ※ This is an alias for Object.defineProperty.
414 */
415 defineProperty: defineOwnProperty,
416
417 /**
418 * Marks the provided object as non·extensible and marks all its
419 * properties as nonconfigurable and (if data properties)
420 * nonwritable, and returns the object.
421 *
422 * ※ This is an alias for Object.freeze.
423 */
424 freeze,
425
426 /**
427 * Returns the property descriptor for the own property with the
428 * provided property key on the provided object, or null if none
429 * exists.
430 *
431 * ※ This is an alias for Object.getOwnPropertyDescriptor.
432 */
433 getOwnPropertyDescriptor,
434
435 /**
436 * Returns the property descriptors for the own properties on the
437 * provided object.
438 *
439 * ※ This is an alias for Object.getOwnPropertyDescriptors.
440 */
441 getOwnPropertyDescriptors,
442
443 /**
444 * Returns an array of string‐valued own property keys on the
445 * provided object.
446 *
447 * ☡ This includes both enumerable and non·enumerable properties.
448 *
449 * ※ This is an alias for Object.getOwnPropertyNames.
450 */
451 getOwnPropertyNames: getOwnPropertyStrings,
452
453 /**
454 * Returns an array of symbol‐valued own property keys on the
455 * provided object.
456 *
457 * ☡ This includes both enumerable and non·enumerable properties.
458 *
459 * ※ This is an alias for Object.getOwnPropertySymbols.
460 */
461 getOwnPropertySymbols,
462
463 /**
464 * Returns the prototype of the provided object.
465 *
466 * ※ This is an alias for Object.getPrototypeOf.
467 */
468 getPrototypeOf: getPrototype,
469
470 /**
471 * Returns whether the provided object has an own property with the
472 * provided property key.
473 *
474 * ※ This is an alias for Object.hasOwn.
475 */
476 hasOwn: hasOwnProperty,
477
478 /**
479 * Returns whether the provided object is extensible.
480 *
481 * ※ This is an alias for Object.isExtensible.
482 */
483 isExtensible,
484
485 /**
486 * Returns whether the provided object is frozen.
487 *
488 * ※ This is an alias for Object.isFrozen.
489 */
490 isFrozen,
491
492 /**
493 * Returns whether the provided object is sealed.
494 *
495 * ※ This is an alias for Object.isSealed.
496 */
497 isSealed,
498
499 /**
500 * Returns an array of key~value pairs for the enumerable,
501 * string‐valued property keys on the provided object.
502 *
503 * ※ This is an alias for Object.entries.
504 */
505 entries: namedEntries,
506
507 /**
508 * Returns an array of the enumerable, string‐valued property keys on
509 * the provided object.
510 *
511 * ※ This is an alias for Object.keys.
512 */
513 keys: namedKeys,
514
515 /**
516 * Returns an array of property values for the enumerable,
517 * string‐valued property keys on the provided object.
518 *
519 * ※ This is an alias for Object.values.
520 */
521 values: namedValues,
522
523 /**
524 * Returns a new object with the provided prototype and property
525 * descriptors.
526 *
527 * ※ This is an alias for Object.create.
528 */
529 create: objectCreate,
530
531 /**
532 * Returns a new object with the provided property keys and values.
533 *
534 * ※ This is an alias for Object.fromEntries.
535 */
536 fromEntries: objectFromEntries,
537
538 /**
539 * Marks the provided object as non·extensible, and returns the
540 * object.
541 *
542 * ※ This is an alias for Object.preventExtensions.
543 */
544 preventExtensions,
545
546 /**
547 * Marks the provided object as non·extensible and marks all its
548 * properties as nonconfigurable, and returns the object.
549 *
550 * ※ This is an alias for Object.seal.
551 */
552 seal,
553
554 /**
555 * Sets the values of the enumerable own properties of the provided
556 * additional objects on the provided object.
557 *
558 * ※ This is an alias for Object.assign.
559 */
560 assign: setPropertyValues,
561
562 /**
563 * Sets the prototype of the provided object to the provided value
564 * and returns the object.
565 *
566 * ※ This is an alias for Object.setPrototypeOf.
567 */
568 setPrototypeOf: setPrototype,
569 } = Object;
570
571 export const {
572 /**
573 * Removes the provided property key from the provided object and
574 * returns the object.
575 *
576 * ☡ This function differs from Reflect.deleteProperty and the
577 * `delete` operator in that it throws if the deletion is
578 * unsuccessful.
579 */
580 deleteOwnProperty,
581
582 /**
583 * Sets the provided property key to the provided value on the
584 * provided object and returns the object.
585 *
586 * ※ This function differs from Reflect.set in that it throws if the
587 * setting is unsuccessful.
588 */
589 setPropertyValue,
590 } = (() => {
591 const { deleteProperty, set } = Reflect;
592
593 return {
594 deleteOwnProperty: (O, P) => {
595 if (!deleteProperty(O, P)) {
596 throw new TypeError(
597 `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`,
598 );
599 } else {
600 return O;
601 }
602 },
603 setPropertyValue: (O, P, V, Receiver = O) => {
604 if (!set(O, P, V, Receiver)) {
605 throw new TypeError(
606 `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`,
607 );
608 } else {
609 return O;
610 }
611 },
612 };
613 })();
614
615 export const {
616 /**
617 * Returns a new frozen shallow copy of the enumerable own properties
618 * of the provided object, according to the following rules :—
619 *
620 * - For data properties, create a nonconfigurable, nonwritable
621 * property with the same value.
622 *
623 * - For accessor properties, create a nonconfigurable accessor
624 * property with the same getter *and* setter.
625 *
626 * The prototype for the resulting object will be taken from the
627 * `prototype` property of the provided constructor, or the
628 * `prototype` of the `constructor` of the provided object if the
629 * provided constructor is undefined. If the used constructor has a
630 * nonnullish `Symbol.species`, that will be used instead. If the
631 * used constructor or species is nullish or does not have a
632 * `prototype` property, the prototype is set to null.
633 */
634 frozenCopy,
635 } = (() => {
636 const {
637 iterator: iteratorSymbol,
638 species: speciesSymbol,
639 } = Symbol;
640 const {
641 next: generatorIteratorNext,
642 } = getPrototype(function* () {}.prototype);
643 const propertyDescriptorEntryIterablePrototype = {
644 [iteratorSymbol]() {
645 return {
646 next: bind(generatorIteratorNext, this.generator(), []),
647 };
648 },
649 };
650 return {
651 frozenCopy: (O, constructor = O?.constructor) => {
652 if (O == null) {
653 // O is null or undefined.
654 throw new TypeError(
655 "Piscēs: Cannot copy properties of null or undefined.",
656 );
657 } else {
658 // O is not null or undefined.
659 //
660 // (If not provided, the constructor will be the value of
661 // getting the `constructor` property of O.)
662 const species = constructor?.[speciesSymbol] ?? constructor;
663 return preventExtensions(
664 objectCreate(
665 species == null || !("prototype" in species)
666 ? null
667 : species.prototype,
668 objectFromEntries(
669 objectCreate(
670 propertyDescriptorEntryIterablePrototype,
671 {
672 generator: {
673 value: function* () {
674 const ownPropertyKeys = getOwnPropertyKeys(O);
675 for (
676 let i = 0;
677 i < ownPropertyKeys.length;
678 ++i
679 ) {
680 const P = ownPropertyKeys[i];
681 const Desc = getOwnPropertyDescriptor(O, P);
682 if (Desc.enumerable) {
683 // P is an enumerable property.
684 yield [
685 P,
686 "get" in Desc || "set" in Desc
687 ? {
688 configurable: false,
689 enumerable: true,
690 get: Desc.get,
691 set: Desc.set,
692 }
693 : {
694 configurable: false,
695 enumerable: true,
696 value: Desc.value,
697 writable: false,
698 },
699 ];
700 } else {
701 // P is not an enumerable property.
702 /* do nothing */
703 }
704 }
705 },
706 },
707 },
708 ),
709 ),
710 ),
711 );
712 }
713 },
714 };
715 })();
716
717 export const {
718 /**
719 * Returns an array of property keys on the provided object.
720 *
721 * ※ This is an alias for Reflect.ownKeys.
722 */
723 ownKeys: getOwnPropertyKeys,
724
725 /**
726 * Returns the value of the provided property key on the provided
727 * object.
728 *
729 * ※ This is an alias for Reflect.get.
730 */
731 get: getPropertyValue,
732
733 /**
734 * Returns whether the provided property key exists on the provided
735 * object.
736 *
737 * ※ This is an alias for Reflect.has.
738 *
739 * ※ This includes properties present on the prototype chain.
740 */
741 has: hasProperty,
742 } = Reflect;
743
744 /**
745 * Returns the provided value converted to an object.
746 *
747 * Null and undefined are converted to a new, empty object. Other
748 * primitives are wrapped. Existing objects are returned with no
749 * modification.
750 *
751 * ※ This is effectively a nonconstructible version of the Object
752 * constructor.
753 */
754 export const { toObject } = (() => {
755 const makeObject = Object;
756 return { toObject: ($) => makeObject($) };
757 })();
758
759 /**
760 * Returns the property key (symbol or string) corresponding to the
761 * provided value.
762 */
763 export const toPropertyKey = ($) => {
764 const key = toPrimitive($, "string");
765 return typeof key == "symbol" ? key : `${key}`;
766 };
This page took 0.164877 seconds and 5 git commands to generate.