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