]> Lady’s Gitweb - Etiquette/commitdiff
Give Tag constructors a superclass
authorLady <redacted>
Wed, 14 Jun 2023 04:42:16 +0000 (21:42 -0700)
committerLady <redacted>
Sun, 18 Jun 2023 23:39:35 +0000 (16:39 -0700)
This “simplifies” the static method definitions on Tag constructors
by giving the constructors themselves private fields and using a method
implementation common to all of them (on their prototype chain), rather
than manually defining each with partial application during initial
constructor generation.

This is also somewhat desirable as it means that the expected situation
of :—

```js
Object.getPrototypeOf(Tag).prototype ==
  Object.getPrototypeOf(Object.getPrototypeOf(new Tag))
```

—: is true, which was not formerly the case.

deps.js
model.js
model.test.js

diff --git a/deps.js b/deps.js
index fa371befa424f20d5920be746d04c1ef58e2d7fa..657026d12aae2410aff8d32576e04831fb90bbca 100644 (file)
--- a/deps.js
+++ b/deps.js
@@ -11,3 +11,4 @@ export {
   wrmgBase32Binary,
   wrmgBase32String,
 } from "https://git.ladys.computer/Pisces/blob_plain/0.3.1:/binary.js";
+export { identity } from "https://git.ladys.computer/Pisces/blob_plain/0.3.1:/function.js";
index 3f0b6adea4b7cd3281070a62b314df26be673f08..d64f6c4218889f7cab3c60dbc215fec884fe6ead 100644 (file)
--- a/model.js
+++ b/model.js
@@ -7,6 +7,7 @@
 // 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/>.
 
+import { identity } from "./deps.js";
 import { Storage } from "./memory.js";
 import { taggingDiscoveryContext } from "./names.js";
 import schema from "./schema.js";
@@ -316,6 +317,7 @@ class Tag {
       );
     };
     Object.defineProperties(constructor, {
+      name: { value: "TagSystem::Tag" },
       prototype: {
         configurable: false,
         enumerable: false,
@@ -324,19 +326,28 @@ class Tag {
           Object.fromEntries(Array.from(
             function* () {
               for (const key in objectProperties) {
+                // Iterate over each object property and yield any
+                // necessary method definitions.
                 const {
                   inverseOf,
                   subPropertyOf,
                 } = objectProperties[key];
                 if (key in transitiveProperties) {
+                  // The current key indicates a transitive property.
+                  //
                   // Transitive property methods are added by their
                   // nontransitive subproperties.
                   /* do nothing */
                 } else {
+                  // The current key does not indicate a transitive
+                  // property.
                   yield [`${key}Tags`, function* () {
                     yield* this.#yieldTags(key);
                   }];
                   if (inverseOf == null) {
+                    // The current key does not indicate an inverse
+                    // property, so add and delete methods are also
+                    // added.
                     const cased = key[0].toUpperCase() +
                       key.substring(1);
                     yield [`add${cased}Tag`, function (...tags) {
@@ -346,12 +357,16 @@ class Tag {
                       return this.#deleteTag(key, ...tags);
                     }];
                   } else {
+                    // The current key indicates an inverse property,
+                    // so no add and delete methods are necessary.
                     /* do nothing */
                   }
                   if (
                     subPropertyOf != null &&
                     subPropertyOf in transitiveProperties
                   ) {
+                    // The current key indicates a subproperty of a
+                    // transitive property; its method is also added.
                     yield [`${subPropertyOf}Tags`, function* () {
                       yield* this.#yieldTransitiveTags(
                         subPropertyOf,
@@ -359,12 +374,17 @@ class Tag {
                       );
                     }];
                   } else {
+                    // The current key does not indicate a subproperty
+                    // of a transitive property.
                     /* do nothing */
                   }
                 }
               }
               for (const key in dataProperties) {
+                // Iterate over each data property and yield any
+                // necessary method definitions.
                 if (key != "prefLabel") {
+                  // The current key is not `"prefLabel"`.
                   const cased = key[0].toUpperCase() +
                     key.substring(1);
                   yield [`${key}s`, function* () {
@@ -377,6 +397,8 @@ class Tag {
                     return this.#deleteLabel(key, ...labels);
                   }];
                 } else {
+                  // The current key is `"prefLabel"`. This is a
+                  // special case which is not handled by the schema.
                   /* do nothing */
                 }
               }
@@ -394,137 +416,21 @@ class Tag {
         writable: false,
       },
     });
-    return Object.defineProperties(
-      constructor,
-      Object.fromEntries([
-        ["name", { value: "TagSystem::Tag" }],
-        ...[
-          "all",
-          "fromIRI",
-          "fromIdentifier",
-          "fromTagURI",
-          "identifiers",
-          Storage.toInstance,
-        ].map((key) => [key, {
-          configurable: true,
-          enumerable: false,
-          value: Object.defineProperty(
-            Tag[key].bind(constructor, system, storage),
-            "name",
-            { value: String(key) },
-          ),
-          writable: true,
-        }]),
-      ]),
-    );
-  }
-
-  /**
-   * Yields the tags in the `TagSystem` associated with this
-   * constructor.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   */
-  static *all(system, storage) {
-    for (const instance of storage.values()) {
-      // Iterate over the entries and yield the ones which are `Tag`s
-      // in this `TagSystem`.
-      if (Tag.getSystem(instance) == system) {
-        // The current instance is a `Tag` in this `TagSystem`.
-        yield instance;
-      } else {
-        // The current instance is not a `Tag` in this `TagSystem`.
-        /* do nothing */
-      }
-    }
-  }
-
-  /**
-   * Returns a new `Tag` resolved from the provided I·R·I.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   *
-   * ☡ This function throws if the I·R·I is not in the `.iriSpace` of
-   * the `TagSystem` associated with this constructor.
-   *
-   * ※ If the I·R·I is not recognized, this function returns `null`.
-   */
-  static fromIRI(system, storage, iri) {
-    const name = `${iri}`;
-    const prefix = `${system.iriSpace}`;
-    if (!name.startsWith(prefix)) {
-      // The I·R·I does not begin with the expected prefix.
-      throw new RangeError(
-        `I·R·I did not begin with the expected prefix: ${iri}`,
-      );
-    } else {
-      // The I·R·I begins with the expected prefix.
-      const identifier = name.substring(prefix.length);
-      try {
-        // Attempt to resolve the identifier.
-        const instance = storage.get(identifier);
-        return Tag.getSystem(instance) == system ? instance : null;
-      } catch {
-        // Do not throw for bad identifiers.
-        return null;
-      }
-    }
-  }
-
-  /**
-   * Returns a new `Tag` resolved from the provided identifier.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   *
-   * ☡ This function throws if the identifier is invalid.
-   *
-   * ※ If the identifier is valid but not recognized, this function
-   * returns `null`.
-   */
-  static fromIdentifier(system, storage, identifier) {
-    const instance = storage.get(identifier);
-    return Tag.getSystem(instance) == system ? instance : null;
+    return new TagConstructor(constructor, system, storage, schema);
   }
 
   /**
-   * Returns a new `Tag` resolved from the provided Tag U·R·I.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
+   * Assigns the provided data and identifier to the provided tag.
    *
-   * ☡ This function throws if the provided Tag U·R·I does not match
-   * the tagging entity of this constructor’s `TagSystem`.
+   * ☡ This function throws if the provided tag is not a `Tag`.
    *
-   * ※ If the specific component of the Tag U·R·I is not recognized,
-   * this function returns `null`.
+   * ※ This function is not exposed.
    */
-  static fromTagURI(system, storage, tagURI) {
-    const tagName = `${tagURI}`;
-    const tagPrefix = `tag:${system.taggingEntity}:`;
-    if (!tagName.startsWith(tagPrefix)) {
-      // The Tag U·R·I does not begin with the expected prefix.
-      throw new RangeError(
-        `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
-      );
-    } else {
-      // The I·R·I begins with the expected prefix.
-      const identifier = tagName.substring(tagPrefix.length);
-      try {
-        // Attempt to resolve the identifier.
-        const instance = storage.get(identifier);
-        return Tag.getSystem(instance) == system ? instance : null;
-      } catch {
-        // Do not throw for bad identifiers.
-        return null;
-      }
-    }
+  static assignData(tag, data, identifier) {
+    tag.#identifier = `${identifier}`;
+    tag.#persistedData = tagData(data);
+    tag.#data = tagData(data);
+    return tag;
   }
 
   /**
@@ -539,47 +445,6 @@ class Tag {
     return !(#system in Object($)) ? null : $.#system;
   }
 
-  /**
-   * Yields the tag identifiers in the `TagSystem` associated with this
-   * constructor.
-   *
-   * ※ The first two arguments of this function are bound when
-   * generating the value of `TagSystem::Tag`. It isn’t possible to
-   * access this function in its unbound form from outside this module.
-   */
-  static *identifiers(system, storage) {
-    for (const [identifier, instance] of storage.entries()) {
-      // Iterate over the entries and yield the ones which are `Tag`s
-      // in this `TagSystem`.
-      if (Tag.getSystem(instance) == system) {
-        // The current instance is a `Tag` in this `TagSystem`.
-        yield identifier;
-      } else {
-        // The current instance is not a `Tag` in this `TagSystem`.
-        /* do nothing */
-      }
-    }
-  }
-
-  /**
-   * Returns a new `Tag` constructed from the provided data and with
-   * the provided identifier.
-   *
-   * ※ This function will not work if called directly from `Tag` (and
-   * nor is it available *to* be called as such from outside this
-   * module). It must be called from a `TagSystem::Tag` bound
-   * constructor.
-   *
-   * ※ This function is not really intended for public usage.
-   */
-  static [Storage.toInstance](_system, _storage, data, identifier) {
-    const tag = new this(data.kind);
-    tag.#identifier = `${identifier}`;
-    tag.#persistedData = tagData(data);
-    tag.#data = tagData(data);
-    return tag;
-  }
-
   static {
     // Overwrite the default `::constructor` method to instead give the
     // actual (bound) constructor which was used to generate a given
@@ -930,6 +795,221 @@ class Tag {
   }
 }
 
+const {
+  /**
+   * A `Tag` constructor function.
+   *
+   * This class extends the identity function, meaning that the object
+   * provided as the constructor is used verbatim (with new private
+   * fields added).
+   *
+   * ※ The instance methods of this class are provided as static
+   * methods on the superclass which all `Tag` constructors inherit
+   * from.
+   *
+   * ※ This class is not exposed.
+   */
+  TagConstructor,
+
+  /**
+   * The exposed constructor function from which all `Tag` constructors
+   * inherit.
+   *
+   * ☡ This constructor always throws.
+   */
+  TagSuper,
+} = (() => {
+  const tagConstructorBehaviours = Object.create(null);
+  return {
+    TagConstructor: class extends identity {
+      /**
+       * The `TagSystem` used for `Tag`s constructed by this
+       * constructor.
+       */
+      #system;
+
+      /** The `Storage` managed by this constructor’s `TagSystem`. */
+      #storage;
+
+      /** The schema in use for this constructor. */
+      #schema;
+
+      /**
+       * Constructs a new `Tag` constructor by adding the appropriate
+       * private fields to the provided constructor, setting its
+       * prototype, and then returning it.
+       *
+       * ※ This constructor does not modify the `name` or `prototype`
+       * properties of the provided constructor.
+       *
+       * ※ See `Tag.For`, where this constructor is used.
+       */
+      constructor(constructor, system, storage, schema) {
+        super(constructor);
+        Object.setPrototypeOf(this, TagSuper);
+        this.#system = system;
+        this.#storage = storage;
+        this.#schema = schema;
+      }
+
+      static {
+        // Define the superclass constructor which all `Tag`
+        // constructors will inherit from.
+        const superclass = tagConstructorBehaviours.TagSuper =
+          function Tag() {
+            throw new TypeError("Tags must belong to a System.");
+          };
+        const { prototype: methods } = this;
+        delete methods.constructor;
+        Object.defineProperty(superclass, "prototype", {
+          configurable: false,
+          enumerable: false,
+          value: Tag.prototype,
+          writable: false,
+        });
+        Object.defineProperties(
+          superclass,
+          Object.getOwnPropertyDescriptors(methods),
+        );
+      }
+
+      /**
+       * Yields the tags in the `TagSystem` associated with this
+       * constructor.
+       */
+      *all() {
+        const system = this.#system;
+        const storage = this.#storage;
+        for (const instance of storage.values()) {
+          // Iterate over the entries and yield the ones which are
+          // `Tag`s in this `TagSystem`.
+          if (Tag.getSystem(instance) == system) {
+            // The current instance is a `Tag` in this `TagSystem`.
+            yield instance;
+          } else {
+            // The current instance is not a `Tag` in this
+            // `TagSystem`.
+            /* do nothing */
+          }
+        }
+      }
+
+      /**
+       * Returns a new `Tag` resolved from the provided I·R·I.
+       *
+       * ☡ This function throws if the I·R·I is not in the `.iriSpace`
+       * of the `TagSystem` associated with this constructor.
+       *
+       * ※ If the I·R·I is not recognized, this function returns
+       * `null`.
+       */
+      fromIRI(iri) {
+        const system = this.#system;
+        const storage = this.#storage;
+        const name = `${iri}`;
+        const prefix = `${system.iriSpace}`;
+        if (!name.startsWith(prefix)) {
+          // The I·R·I does not begin with the expected prefix.
+          throw new RangeError(
+            `I·R·I did not begin with the expected prefix: ${iri}`,
+          );
+        } else {
+          // The I·R·I begins with the expected prefix.
+          const identifier = name.substring(prefix.length);
+          try {
+            // Attempt to resolve the identifier.
+            const instance = storage.get(identifier);
+            return Tag.getSystem(instance) == system ? instance : null;
+          } catch {
+            // Do not throw for bad identifiers.
+            return null;
+          }
+        }
+      }
+
+      /**
+       * Returns a new `Tag` resolved from the provided identifier.
+       *
+       * ☡ This function throws if the identifier is invalid.
+       *
+       * ※ If the identifier is valid but not recognized, this
+       * function returns `null`.
+       */
+      fromIdentifier(identifier) {
+        const system = this.#system;
+        const storage = this.#storage;
+        const instance = storage.get(identifier);
+        return Tag.getSystem(instance) == system ? instance : null;
+      }
+
+      /**
+       * Returns a new `Tag` resolved from the provided Tag U·R·I.
+       *
+       * ☡ This function throws if the provided Tag U·R·I does not
+       * match the tagging entity of this constructor’s `TagSystem`.
+       *
+       * ※ If the specific component of the Tag U·R·I is not
+       * recognized, this function returns `null`.
+       */
+      fromTagURI(tagURI) {
+        const system = this.#system;
+        const storage = this.#storage;
+        const tagName = `${tagURI}`;
+        const tagPrefix = `tag:${system.taggingEntity}:`;
+        if (!tagName.startsWith(tagPrefix)) {
+          // The Tag U·R·I does not begin with the expected prefix.
+          throw new RangeError(
+            `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
+          );
+        } else {
+          // The I·R·I begins with the expected prefix.
+          const identifier = tagName.substring(tagPrefix.length);
+          try {
+            // Attempt to resolve the identifier.
+            const instance = storage.get(identifier);
+            return Tag.getSystem(instance) == system ? instance : null;
+          } catch {
+            // Do not throw for bad identifiers.
+            return null;
+          }
+        }
+      }
+
+      /**
+       * Yields the tag identifiers in the `TagSystem` associated with
+       * this constructor.
+       */
+      *identifiers() {
+        const system = this.#system;
+        const storage = this.#storage;
+        for (const [identifier, instance] of storage.entries()) {
+          // Iterate over the entries and yield the ones which are
+          // `Tag`s in this `TagSystem`.
+          if (Tag.getSystem(instance) == system) {
+            // The current instance is a `Tag` in this `TagSystem`.
+            yield identifier;
+          } else {
+            // The current instance is not a `Tag` in this `TagSystem`.
+            /* do nothing */
+          }
+        }
+      }
+
+      /**
+       * Returns a new `Tag` constructed from the provided data and
+       * with the provided identifier.
+       *
+       * ※ This function is not really intended for public usage.
+       */
+      [Storage.toInstance](data, identifier) {
+        const tag = new this(data.kind);
+        return Tag.assignData(tag, data, identifier);
+      }
+    },
+    TagSuper: tagConstructorBehaviours.TagSuper,
+  };
+})();
+
 const {
   /**
    * Returns whether the provided schema, subject class, object
index 74f73daf349f5c068405994f03729d4e36e5fe57..a92671c52d2a4a033ab575172ff6abe7e00f2117 100644 (file)
@@ -140,12 +140,6 @@ describe("TagSystem", () => {
       });
     });
 
-    describe(".For", () => {
-      it("[[Has]] is not present", () => {
-        assertFalse("For" in Tag);
-      });
-    });
-
     describe(".all", () => {
       it("[[Call]] yields all the persisted tags", () => {
         const tags = new Set(function* () {
@@ -170,6 +164,12 @@ describe("TagSystem", () => {
       });
     });
 
+    describe(".constructor", () => {
+      it("[[Get]] is `Function`", () => {
+        assertStrictEquals(Tag.constructor, Function);
+      });
+    });
+
     describe(".fromIRI", () => {
       it("[[Call]] returns the persisted tag with the given I·R·I", () => {
         const tag = new Tag();
@@ -262,12 +262,6 @@ describe("TagSystem", () => {
       });
     });
 
-    describe(".getSystem", () => {
-      it("[[Has]] is not present", () => {
-        assertFalse("getSystem" in Tag);
-      });
-    });
-
     describe(".identifiers", () => {
       it("[[Call]] yields all the persisted identifiers", () => {
         const tags = new Set(function* () {
This page took 0.035202 seconds and 4 git commands to generate.