]> Lady’s Gitweb - Pisces/blob - object.js
Basic functions for object & function manipulation
[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 { call } from "./function.js";
11
12 export const {
13 assign: assignProperties,
14 defineProperty: defineOwnProperty,
15 defineProperties: defineOwnProperties,
16 freeze,
17 getOwnPropertyDescriptor,
18 getOwnPropertyDescriptors,
19 getOwnPropertyNames: getOwnPropertyStrings,
20 getOwnPropertySymbols,
21 getPrototypeOf: getPrototype,
22 hasOwn: hasOwnProperty,
23 isExtensible,
24 isFrozen,
25 isSealed,
26 entries: namedEntries,
27 keys: namedKeys,
28 values: namedValues,
29 create: objectCreate,
30 fromEntries: objectFromEntries,
31 preventExtensions,
32 is: sameValue,
33 seal,
34 setPrototypeOf: setPrototype,
35 } = Object;
36
37 export const {
38 delete: deleteOwnProperty,
39 keys: getOwnPropertyKeys,
40 get: getPropertyValue,
41 has: hasProperty,
42 set: setPropertyValue,
43 } = Reflect;
44
45 /**
46 * A property descriptor object.
47 *
48 * Actually constructing a property descriptor object using this class
49 * is only necessary if you need strict guarantees about the types of
50 * its properties; the resulting object is proxied to ensure the types
51 * match what one would expect from composing FromPropertyDescriptor
52 * and ToPropertyDescriptor in the Ecmascript specification.
53 *
54 * Otherwise, the instance properties and methods are generic.
55 */
56 export const PropertyDescriptor = (() => {
57 class PropertyDescriptor extends null {
58 /**
59 * Constructs a new property descriptor object from the provided
60 * object.
61 *
62 * The resulting object is proxied to enforce types (for example,
63 * its `enumerable` property, if defined, will always be a
64 * boolean).
65 */
66 //deno-lint-ignore constructor-super
67 constructor(Obj) {
68 if (!isObject(Obj)) {
69 // The provided value is not an object.
70 throw new TypeError(
71 "Piscēs: Cannot convert primitive to property descriptor.",
72 );
73 } else {
74 // The provided value is an object.
75 const desc = objectCreate(propertyDescriptorPrototype);
76 if ("enumerable" in Obj) {
77 // An enumerable property is specified.
78 desc.enumerable = !!Obj.enumerable;
79 } else {
80 // An enumerable property is not specified.
81 /* do nothing */
82 }
83 if ("configurable" in Obj) {
84 // A configurable property is specified.
85 desc.configurable = !!Obj.configurable;
86 } else {
87 // A configurable property is not specified.
88 /* do nothing */
89 }
90 if ("value" in Obj) {
91 // A value property is specified.
92 desc.value = Obj.value;
93 } else {
94 // A value property is not specified.
95 /* do nothing */
96 }
97 if ("writable" in Obj) {
98 // A writable property is specified.
99 desc.writable = !!Obj.writable;
100 } else {
101 // A writable property is not specified.
102 /* do nothing */
103 }
104 if ("get" in Obj) {
105 // A get property is specified.
106 const getter = Obj.get;
107 if (typeof getter != "function") {
108 // The getter is not callable.
109 throw new TypeError("Piscēs: Getters must be callable.");
110 } else {
111 // The getter is callable.
112 desc.get = getter;
113 }
114 } else {
115 // A get property is not specified.
116 /* do nothing */
117 }
118 if ("set" in Obj) {
119 // A set property is specified.
120 const setter = Obj.set;
121 if (typeof setter != "function") {
122 // The setter is not callable.
123 throw new TypeError("Piscēs: Setters must be callable.");
124 } else {
125 // The setter is callable.
126 desc.set = setter;
127 }
128 } else {
129 // A set property is not specified.
130 /* do nothing */
131 }
132 if (
133 ("get" in desc || "set" in desc) &&
134 ("value" in desc || "writable" in desc)
135 ) {
136 // Both accessor and data attributes have been defined.
137 throw new TypeError(
138 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
139 );
140 } else {
141 // The property descriptor is valid.
142 return new Proxy(desc, propertyDescriptorProxyHandler);
143 }
144 }
145 }
146
147 /**
148 * Completes this property descriptor by setting missing values to
149 * their defaults.
150 *
151 * This method modifies this object and returns undefined.
152 */
153 complete() {
154 if (this !== undefined && !("get" in this || "set" in this)) {
155 // This is a generic or data descriptor.
156 if (!("value" in this)) {
157 // `value` is not defined on this.
158 this.value = undefined;
159 } else {
160 // `value` is already defined on this.
161 /* do nothing */
162 }
163 if (!("writable" in this)) {
164 // `writable` is not defined on this.
165 this.writable = false;
166 } else {
167 // `writable` is already defined on this.
168 /* do nothing */
169 }
170 } else {
171 // This is not a generic or data descriptor.
172 if (!("get" in this)) {
173 // `get` is not defined on this.
174 this.get = undefined;
175 } else {
176 // `get` is already defined on this.
177 /* do nothing */
178 }
179 if (!("set" in this)) {
180 // `set` is not defined on this.
181 this.set = undefined;
182 } else {
183 // `set` is already defined on this.
184 /* do nothing */
185 }
186 }
187 if (!("enumerable" in this)) {
188 // `enumerable` is not defined on this.
189 this.enumerable = false;
190 } else {
191 // `enumerable` is already defined on this.
192 /* do nothing */
193 }
194 if (!("configurable" in this)) {
195 // `configurable` is not defined on this.
196 this.configurable = false;
197 } else {
198 // `configurable` is already defined on this.
199 /* do nothing */
200 }
201 }
202
203 /** Returns whether this is an accessor descrtiptor. */
204 get isAccessorDescriptor() {
205 return this !== undefined && ("get" in this || "set" in this);
206 }
207
208 /** Returns whether this is a data descrtiptor. */
209 get isDataDescriptor() {
210 return this !== undefined &&
211 ("value" in this || "writable" in this);
212 }
213
214 /**
215 * Returns whether this is a fully‐populated property descriptor.
216 */
217 get isFullyPopulated() {
218 return this !== undefined &&
219 ("value" in this && "writable" in this ||
220 "get" in this && "set" in this) &&
221 "enumerable" in this && "configurable" in this;
222 }
223
224 /**
225 * Returns whether this is a generic (not accessor or data)
226 * descrtiptor.
227 */
228 get isGenericDescriptor() {
229 return this !== undefined &&
230 !("get" in this || "set" in this || "value" in this ||
231 "writable" in this);
232 }
233 }
234
235 const coercePropretyDescriptorValue = (P, V) => {
236 switch (P) {
237 case "configurable":
238 case "enumerable":
239 case "writable":
240 return !!V;
241 case "value":
242 return V;
243 case "get":
244 if (typeof V != "function") {
245 throw new TypeError(
246 "Piscēs: Getters must be callable.",
247 );
248 } else {
249 return V;
250 }
251 case "set":
252 if (typeof V != "function") {
253 throw new TypeError(
254 "Piscēs: Setters must be callable.",
255 );
256 } else {
257 return V;
258 }
259 default:
260 return V;
261 }
262 };
263
264 const propertyDescriptorPrototype = PropertyDescriptor.prototype;
265
266 const propertyDescriptorProxyHandler = assignProperties(
267 objectCreate(null),
268 {
269 defineProperty(O, P, Desc) {
270 if (
271 P === "configurable" || P === "enumerable" ||
272 P === "writable" || P === "value" ||
273 P === "get" || P === "set"
274 ) {
275 // P is a property descriptor attribute.
276 const desc = new PropertyDescriptor(Desc);
277 if ("get" in desc || "set" in desc) {
278 // Desc is an accessor property descriptor.
279 throw new TypeError(
280 "Piscēs: Property descriptor attributes must be data properties.",
281 );
282 } else if ("value" in desc) {
283 // Desc has a value.
284 desc.value = coercePropretyDescriptorValue(P, desc.value);
285 } else {
286 // Desc is not an accessor property descriptor and has no
287 // value.
288 /* do nothing */
289 }
290 const isAccessorDescriptor = "get" === P || "set" === P ||
291 "get" in O || "set" in O;
292 const isDataDescriptor = "value" === P || "writable" === P ||
293 "value" in O || "writable" in O;
294 if (isAccessorDescriptor && isDataDescriptor) {
295 // Both accessor and data attributes will be present on O
296 // after defining P.
297 throw new TypeError(
298 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
299 );
300 } else {
301 // P can be safely defined on O.
302 return defineOwnProperty(O, P, desc);
303 }
304 } else {
305 // P is not a property descriptor attribute.
306 return defineOwnProperty(O, P, Desc);
307 }
308 },
309 set(O, P, V, Receiver) {
310 const newValue = coercePropertyDescriptorValue(P, V);
311 const isAccessorDescriptor = "get" === P || "set" === P ||
312 "get" in O || "set" in O;
313 const isDataDescriptor = "value" === P || "writable" === P ||
314 "value" in O || "writable" in O;
315 if (isAccessorDescriptor && isDataDescriptor) {
316 // Both accessor and data attributes will be present on O
317 // after defining P.
318 throw new TypeError(
319 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
320 );
321 } else {
322 // P can be safely defined on O.
323 return setPropertyValue(O, prop, newValue, Receiver);
324 }
325 },
326 setPrototypeOf(O, V) {
327 if (V !== propertyDescriptorPrototype) {
328 // V is not the property descriptor prototype.
329 return false;
330 } else {
331 // V is the property descriptor prototype.
332 return setPrototype(O, V);
333 }
334 },
335 },
336 );
337
338 return PropertyDescriptor;
339 })();
340
341 /**
342 * Returns a new frozen shallow copy of the enumerable own properties
343 * of the provided object, according to the following rules :—
344 *
345 * - For data properties, create a nonconfigurable, nonwritable
346 * property with the same value.
347 *
348 * - For accessor properties, create a nonconfigurable accessor
349 * property with the same getter *and* setter.
350 *
351 * The prototype for the resulting object will be taken from the
352 * `prototype` property of the provided constructor, or the `prototype`
353 * of the `constructor` of the provided object if the provided
354 * constructor is undefined. If the used constructor has a nonnullish
355 * `Symbol.species`, that will be used instead.
356 */
357 export const frozenCopy = (O, constructor = O?.constructor) => {
358 if (O == null) {
359 // O is null or undefined.
360 throw new TypeError(
361 "Piscēs: Cannot copy properties of null or undefined.",
362 );
363 } else {
364 // O is not null or undefined.
365 //
366 // (If not provided, the constructor will be the value of getting
367 // the `constructor` property of O.)
368 const species = constructor?.[Symbol.species] ?? constructor;
369 return preventExtensions(
370 objectCreate(
371 species == null || !("prototype" in species)
372 ? null
373 : species.prototype,
374 objectFromEntries(
375 function* () {
376 for (const P of getOwnPropertyKeys(O)) {
377 const Desc = getOwnPropertyDescriptor(O, P);
378 if (Desc.enumerable) {
379 // P is an enumerable property.
380 yield [
381 P,
382 "get" in Desc || "set" in Desc
383 ? {
384 configurable: false,
385 enumerable: true,
386 get: Desc.get,
387 set: Desc.set,
388 }
389 : {
390 configurable: false,
391 enumerable: true,
392 value: Desc.value,
393 writable: false,
394 },
395 ];
396 } else {
397 // P is not an enumerable property.
398 /* do nothing */
399 }
400 }
401 }(),
402 ),
403 ),
404 );
405 }
406 };
407
408 /** Returns whether the provided value is an object. */
409 export const isObject = ($) => {
410 return $ !== null &&
411 (typeof $ == "function" || typeof $ == "object");
412 };
413
414 /**
415 * Returns the primitive value of the provided object per its
416 * `toString` and `valueOf` methods.
417 *
418 * If the provided hint is "string", then `toString` takes precedence;
419 * otherwise, `valueOf` does.
420 *
421 * Throws an error if both of these methods are not callable or do not
422 * return a primitive.
423 */
424 export const ordinaryToPrimitive = (O, hint) => {
425 for (
426 const name of hint == "string"
427 ? ["toString", "valueOf"]
428 : ["valueOf", "toString"]
429 ) {
430 const method = O[name];
431 if (typeof method == "function") {
432 // Method is callable.
433 const result = call(method, O, []);
434 if (!isObject(result)) {
435 // Method returns a primitive.
436 return result;
437 } else {
438 // Method returns an object.
439 continue;
440 }
441 } else {
442 // Method is not callable.
443 continue;
444 }
445 }
446 throw new TypeError("Piscēs: Unable to convert object to primitive");
447 };
448
449 /**
450 * Returns the provided value converted to a primitive, or throws if
451 * no such conversion is possible.
452 *
453 * The provided preferred type, if specified, should be "string",
454 * "number", or "default". If the provided input has a
455 * `Symbol.toPrimitive` method, this function will throw rather than
456 * calling that method with a preferred type other than one of the
457 * above.
458 */
459 export const toPrimitive = ($, preferredType) => {
460 if (isObject($)) {
461 // The provided value is an object.
462 const exoticToPrim = $[Symbol.toPrimitive] ?? undefined;
463 if (exoticToPrim !== undefined) {
464 // The provided value has an exotic primitive conversion method.
465 if (typeof exoticToPrim != "function") {
466 // The method is not callable.
467 throw new TypeError(
468 "Piscēs: Symbol.toPrimitive was neither nullish nor callable.",
469 );
470 } else {
471 // The method is callable.
472 const hint = `${preferredType ?? "default"}`;
473 if (!["default", "string", "number"].includes(hint)) {
474 // An invalid preferred type was specified.
475 throw new TypeError(
476 `Piscēs: Invalid preferred type: ${preferredType}.`,
477 );
478 } else {
479 // The resulting hint is either default, string, or number.
480 return call(exoticToPrim, $, [hint]);
481 }
482 }
483 } else {
484 // Use the ordinary primitive conversion function.
485 ordinaryToPrimitive($, hint);
486 }
487 } else {
488 // The provided value is already a primitive.
489 return $;
490 }
491 };
492
493 /**
494 * Returns the property key (symbol or string) corresponding to the
495 * provided value.
496 */
497 export const toPropertyKey = ($) => {
498 const key = toPrimitive($, "string");
499 return typeof key == "symbol" ? key : `${key}`;
500 };
This page took 0.116532 seconds and 5 git commands to generate.