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