]> Lady’s Gitweb - Etiquette/blobdiff - model.js
Give Tag constructors a superclass
[Etiquette] / model.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
This page took 0.031995 seconds and 4 git commands to generate.