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