]> Lady’s Gitweb - Pisces/blobdiff - object.js
PropertyDescriptor and frozenCopy
[Pisces] / object.js
index d08f43a00283853679c32b8dcff76b77350fad54..8f5c4903882ce1fd4b3561cac44c2ce4edd19553 100644 (file)
--- a/object.js
+++ b/object.js
@@ -7,6 +7,369 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
+/**
+ * A property descriptor object.
+ *
+ * Actually constructing a property descriptor object using this class
+ * is only necessary if you need strict guarantees about the types of
+ * its properties; the resulting object is proxied to ensure the types
+ * match what one would expect from composing FromPropertyDescriptor
+ * and ToPropertyDescriptor in the Ecmascript specification.
+ *
+ * Otherwise, the instance properties and methods are generic.
+ */
+export const PropertyDescriptor = (() => {
+  class PropertyDescriptor extends null {
+    /**
+     * Constructs a new property descriptor object from the provided
+     * object.
+     *
+     * The resulting object is proxied to enforce types (for example,
+     * its `enumerable` property, if defined, will always be a
+     * boolean).
+     */
+    //deno-lint-ignore constructor-super
+    constructor(Obj) {
+      if (!isObject(Obj)) {
+        // The provided value is not an object.
+        throw new TypeError(
+          "Piscēs: Cannot convert primitive to property descriptor.",
+        );
+      } else {
+        // The provided value is an object.
+        const desc = Object.create(propertyDescriptorPrototype);
+        if ("enumerable" in Obj) {
+          // An enumerable property is specified.
+          desc.enumerable = !!Obj.enumerable;
+        } else {
+          // An enumerable property is not specified.
+          /* do nothing */
+        }
+        if ("configurable" in Obj) {
+          // A configurable property is specified.
+          desc.configurable = !!Obj.configurable;
+        } else {
+          // A configurable property is not specified.
+          /* do nothing */
+        }
+        if ("value" in Obj) {
+          // A value property is specified.
+          desc.value = Obj.value;
+        } else {
+          // A value property is not specified.
+          /* do nothing */
+        }
+        if ("writable" in Obj) {
+          // A writable property is specified.
+          desc.writable = !!Obj.writable;
+        } else {
+          // A writable property is not specified.
+          /* do nothing */
+        }
+        if ("get" in Obj) {
+          // A get property is specified.
+          const getter = Obj.get;
+          if (typeof getter != "function") {
+            // The getter is not callable.
+            throw new TypeError("Piscēs: Getters must be callable.");
+          } else {
+            // The getter is callable.
+            desc.get = getter;
+          }
+        } else {
+          // A get property is not specified.
+          /* do nothing */
+        }
+        if ("set" in Obj) {
+          // A set property is specified.
+          const setter = Obj.set;
+          if (typeof setter != "function") {
+            // The setter is not callable.
+            throw new TypeError("Piscēs: Setters must be callable.");
+          } else {
+            // The setter is callable.
+            desc.set = setter;
+          }
+        } else {
+          // A set property is not specified.
+          /* do nothing */
+        }
+        if (
+          ("get" in desc || "set" in desc) &&
+          ("value" in desc || "writable" in desc)
+        ) {
+          // Both accessor and data attributes have been defined.
+          throw new TypeError(
+            "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
+          );
+        } else {
+          // The property descriptor is valid.
+          return new Proxy(desc, propertyDescriptorProxyHandler);
+        }
+      }
+    }
+
+    /**
+     * Completes this property descriptor by setting missing values to
+     * their defaults.
+     *
+     * This method modifies this object and returns undefined.
+     */
+    complete() {
+      if (this !== undefined && !("get" in this || "set" in this)) {
+        // This is a generic or data descriptor.
+        if (!("value" in this)) {
+          // `value` is not defined on this.
+          this.value = undefined;
+        } else {
+          // `value` is already defined on this.
+          /* do nothing */
+        }
+        if (!("writable" in this)) {
+          // `writable` is not defined on this.
+          this.writable = false;
+        } else {
+          // `writable` is already defined on this.
+          /* do nothing */
+        }
+      } else {
+        // This is not a generic or data descriptor.
+        if (!("get" in this)) {
+          // `get` is not defined on this.
+          this.get = undefined;
+        } else {
+          // `get` is already defined on this.
+          /* do nothing */
+        }
+        if (!("set" in this)) {
+          // `set` is not defined on this.
+          this.set = undefined;
+        } else {
+          // `set` is already defined on this.
+          /* do nothing */
+        }
+      }
+      if (!("enumerable" in this)) {
+        // `enumerable` is not defined on this.
+        this.enumerable = false;
+      } else {
+        // `enumerable` is already defined on this.
+        /* do nothing */
+      }
+      if (!("configurable" in this)) {
+        // `configurable` is not defined on this.
+        this.configurable = false;
+      } else {
+        // `configurable` is already defined on this.
+        /* do nothing */
+      }
+    }
+
+    /** Returns whether this is an accessor descrtiptor. */
+    get isAccessorDescriptor() {
+      return this !== undefined && ("get" in this || "set" in this);
+    }
+
+    /** Returns whether this is a data descrtiptor. */
+    get isDataDescriptor() {
+      return this !== undefined &&
+        ("value" in this || "writable" in this);
+    }
+
+    /**
+     * Returns whether this is a fully‐populated property descriptor.
+     */
+    get isFullyPopulated() {
+      return this !== undefined &&
+        ("value" in this && "writable" in this ||
+          "get" in this && "set" in this) &&
+        "enumerable" in this && "configurable" in this;
+    }
+
+    /**
+     * Returns whether this is a generic (not accessor or data)
+     * descrtiptor.
+     */
+    get isGenericDescriptor() {
+      return this !== undefined &&
+        !("get" in this || "set" in this || "value" in this ||
+          "writable" in this);
+    }
+  }
+
+  const coercePropretyDescriptorValue = (P, V) => {
+    switch (P) {
+      case "configurable":
+      case "enumerable":
+      case "writable":
+        return !!V;
+      case "value":
+        return V;
+      case "get":
+        if (typeof V != "function") {
+          throw new TypeError(
+            "Piscēs: Getters must be callable.",
+          );
+        } else {
+          return V;
+        }
+      case "set":
+        if (typeof V != "function") {
+          throw new TypeError(
+            "Piscēs: Setters must be callable.",
+          );
+        } else {
+          return V;
+        }
+      default:
+        return V;
+    }
+  };
+
+  const propertyDescriptorPrototype = PropertyDescriptor.prototype;
+
+  const propertyDescriptorProxyHandler = Object.assign(
+    Object.create(null),
+    {
+      defineProperty(O, P, Desc) {
+        if (
+          P === "configurable" || P === "enumerable" ||
+          P === "writable" || P === "value" ||
+          P === "get" || P === "set"
+        ) {
+          // P is a property descriptor attribute.
+          const desc = new PropertyDescriptor(Desc);
+          if ("get" in desc || "set" in desc) {
+            // Desc is an accessor property descriptor.
+            throw new TypeError(
+              "Piscēs: Property descriptor attributes must be data properties.",
+            );
+          } else if ("value" in desc) {
+            // Desc has a value.
+            desc.value = coercePropretyDescriptorValue(P, desc.value);
+          } else {
+            // Desc is not an accessor property descriptor and has no
+            // value.
+            /* do nothing */
+          }
+          const isAccessorDescriptor = "get" === P || "set" === P ||
+            "get" in O || "set" in O;
+          const isDataDescriptor = "value" === P || "writable" === P ||
+            "value" in O || "writable" in O;
+          if (isAccessorDescriptor && isDataDescriptor) {
+            // Both accessor and data attributes will be present on O
+            // after defining P.
+            throw new TypeError(
+              "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
+            );
+          } else {
+            // P can be safely defined on O.
+            return Reflect.defineProperty(O, P, desc);
+          }
+        } else {
+          // P is not a property descriptor attribute.
+          return Reflect.defineProperty(O, P, Desc);
+        }
+      },
+      set(O, P, V, Receiver) {
+        const newValue = coercePropertyDescriptorValue(P, V);
+        const isAccessorDescriptor = "get" === P || "set" === P ||
+          "get" in O || "set" in O;
+        const isDataDescriptor = "value" === P || "writable" === P ||
+          "value" in O || "writable" in O;
+        if (isAccessorDescriptor && isDataDescriptor) {
+          // Both accessor and data attributes will be present on O
+          // after defining P.
+          throw new TypeError(
+            "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
+          );
+        } else {
+          // P can be safely defined on O.
+          return Reflect.set(O, prop, newValue, Receiver);
+        }
+      },
+      setPrototypeOf(O, V) {
+        if (V !== propertyDescriptorPrototype) {
+          // V is not the property descriptor prototype.
+          return false;
+        } else {
+          // V is the property descriptor prototype.
+          return Reflect.setPrototypeOf(O, V);
+        }
+      },
+    },
+  );
+
+  return PropertyDescriptor;
+})();
+
+/**
+ * Returns a new frozen shallow copy of the enumerable own properties
+ * of the provided object, according to the following rules :—
+ *
+ * - For data properties, create a nonconfigurable, nonwritable
+ *   property with the same value.
+ *
+ * - For accessor properties, create a nonconfigurable accessor
+ *   property with the same getter *and* setter.
+ *
+ * The prototype for the resulting object will be taken from the
+ * `prototype` property of the provided constructor, or the `prototype`
+ * of the `constructor` of the provided object if the provided
+ * constructor is undefined. If the used constructor has a nonnullish
+ * `Symbol.species`, that will be used instead.
+ */
+export const frozenCopy = (O, constructor = O?.constructor) => {
+  if (O == null) {
+    // O is null or undefined.
+    throw new TypeError(
+      "Piscēs: Cannot copy properties of null or undefined.",
+    );
+  } else {
+    // O is not null or undefined.
+    //
+    // (If not provided, the constructor will be the value of getting
+    // the `constructor` property of O.)
+    const species = constructor?.[Symbol.species] ?? constructor;
+    return Object.preventExtensions(
+      Object.create(
+        species == null || !("prototype" in species)
+          ? null
+          : species.prototype,
+        Object.fromEntries(
+          function* () {
+            for (const P of Reflect.ownKeys(O)) {
+              const Desc = Object.getOwnPropertyDescriptor(O, P);
+              if (Desc.enumerable) {
+                // P is an enumerable property.
+                yield [
+                  P,
+                  "get" in Desc || "set" in Desc
+                    ? {
+                      configurable: false,
+                      enumerable: true,
+                      get: Desc.get,
+                      set: Desc.set,
+                    }
+                    : {
+                      configurable: false,
+                      enumerable: true,
+                      value: Desc.value,
+                      writable: false,
+                    },
+                ];
+              } else {
+                // P is not an enumerable property.
+                /* do nothing */
+              }
+            }
+          }(),
+        ),
+      ),
+    );
+  }
+};
+
 /** Returns whether the provided value is a constructor. */
 export const isConstructor = ($) => {
   if (!isObject($)) {
This page took 0.034183 seconds and 4 git commands to generate.