// 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";
);
};
Object.defineProperties(constructor, {
+ name: { value: "TagSystem::Tag" },
prototype: {
configurable: false,
enumerable: false,
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) {
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,
);
}];
} 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* () {
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 */
}
}
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,
- }]),
- ]),
- );
+ return new TagConstructor(constructor, system, storage, schema);
}
/**
- * Yields the tags in the `TagSystem` associated with this
- * constructor.
+ * Assigns the provided data and identifier to the provided tag.
*
- * ※ 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.
+ * ☡ This function throws if the provided tag is not a `Tag`.
*
- * ※ 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`.
+ * ※ This function is not exposed.
*/
- static fromIdentifier(system, storage, identifier) {
- const instance = storage.get(identifier);
- return Tag.getSystem(instance) == system ? instance : null;
+ static assignData(tag, data, identifier) {
+ tag.#identifier = `${identifier}`;
+ tag.#persistedData = tagData(data);
+ tag.#data = tagData(data);
+ return tag;
}
/**
- * Returns a new `Tag` resolved from the provided Tag U·R·I.
+ * Returns a new `Tag` with the provided identifier, kind, and
+ * prefLabel.
*
- * ※ 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 exists to enable `TagSystem`s to replay Create
+ * activities, maintaining the identifier of the original.
*
- * ☡ 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 identifier is already in
+ * use.
*
- * ※ 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.
+ static new(system, identifier, kind = "Tag", prefLabel = "") {
+ const storage = (new system.Tag()).#storage;
+ if (storage.has(identifier)) {
throw new RangeError(
- `Tag U·R·I did not begin with the expected prefix: ${tagURI}`,
+ `Cannot create Tag: Identifier already in use: ${identifier}.`,
);
} 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;
- }
+ const createdTag = new system.Tag(kind, prefLabel);
+ createdTag.#identifier = identifier;
+ createdTag.persist(true);
+ return createdTag;
}
}
* ※ This function can be used to check if the provided value has
* private tag features.
*
+ * ※ `Tag::system` is an overridable, publicly‐accessible means of
+ * accessing the system.
+ *
* ※ This function is not exposed.
*/
static getSystem($) {
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
return this.#kind;
}
+ /**
+ * Returns the `TagSystem` for this `Tag`.
+ *
+ * ※ Internally, `Tag.getSystem` is preferred.
+ */
+ get system() {
+ return this.#system;
+ }
+
/**
* Persist this `Tag` to storage and return an ActivityStreams
* serialization of a Tag Activity representing any changes, or
}
}
+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: staticFeatures } = this;
+ delete staticFeatures.constructor;
+ Object.defineProperty(superclass, "prototype", {
+ configurable: false,
+ enumerable: false,
+ value: Tag.prototype,
+ writable: false,
+ });
+ Object.defineProperties(
+ superclass,
+ Object.getOwnPropertyDescriptors(staticFeatures),
+ );
+ }
+
+ /**
+ * 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
+ * `undefined`.
+ */
+ 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
+ : undefined;
+ } catch {
+ // Do not throw for bad identifiers.
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * 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 `undefined`.
+ */
+ fromIdentifier(identifier) {
+ const system = this.#system;
+ const storage = this.#storage;
+ const instance = storage.get(identifier);
+ return Tag.getSystem(instance) == system
+ ? instance
+ : undefined;
+ }
+
+ /**
+ * 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 `undefined`.
+ */
+ 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
+ : undefined;
+ } catch {
+ // Do not throw for bad identifiers.
+ return undefined;
+ }
+ }
+ }
+
+ /**
+ * 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 the `TagSystem` for this `Tag` constructor. */
+ get system() {
+ return this.#system;
+ }
+
+ /**
+ * 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
/** The identifier of this `TagSystem`. */
#identifier;
+ /** The schema used by this `TagSystem. */
+ #schema = schema;
+
/** The internal `Storage` of this `TagSystem`. */
#storage = new Storage();
} else {
// No bound constructor has been created yet.
const storage = this.#storage;
- return this.#Tag = Tag.For(this, storage, schema);
+ return this.#Tag = Tag.For(this, storage, this.#schema);
+ }
+ }
+
+ /**
+ * Applies the provided activity to this `TagSystem` by replaying its
+ * statement changes.
+ *
+ * ※ This method assumes that the provided activity conforms to the
+ * assumptions made by this module; i·e that its tags use the same
+ * identifier format and its activities do not use statements with
+ * inverse property predicates. It is not intended for the generic
+ * playback of activities produced by other scripts or mechanisms,
+ * for which a more sophisticated solution is required.
+ *
+ * ☡ This method throws an error if the provided activity cannot be
+ * processed.
+ */
+ apply(activity) {
+ const { Tag: TagConstructor } = this;
+ const {
+ classes,
+ objectProperties,
+ transitiveProperties,
+ dataProperties,
+ } = this.#schema;
+ const { object, states, unstates } = activity;
+ const activityTypes = [].concat(activity["@type"]);
+ if (!object) {
+ // ☡ The provided activity has no object.
+ throw new TypeError(
+ "Cannot apply activity: Activity lacks an object.",
+ );
+ } else {
+ // The provided activity has an object.
+ const iri = `${object}`;
+ const iriSpace = `${this.iriSpace}`;
+ const identifier = (() => {
+ // Extract the identifier from the object I·R·I.
+ if (!iri.startsWith(iriSpace)) {
+ // ☡ The object of the provided activity is not in the I·R·I
+ // space of this `TagSystem`.
+ throw new RangeError(
+ `Cannot apply activity: Object is not in I·R·I space: ${object}`,
+ );
+ } else {
+ // ☡ The object of the provided activity is in the I·R·I
+ // space of this `TagSystem`.
+ return iri.substring(iriSpace.length);
+ }
+ })();
+ const tag = (() => {
+ // Either resolve the identifier to an existing tag or create
+ // a new one.
+ if (activityTypes.includes("Create")) {
+ // The provided activity is a Create activity.
+ const kind = states.findLast(
+ ({ predicate, object }) =>
+ predicate == "a" && `${object}` in classes,
+ )?.object;
+ if (kind == null) {
+ // ☡ There is no recognized tag class provided for the tag;
+ // it cannot be created.
+ throw new RangeError(
+ `Cannot apply activity: Tag type not recognized.`,
+ );
+ } else {
+ // There is a recognized tag class provided for the tag.
+ return Tag.new(this, identifier, kind);
+ }
+ } else {
+ // The provided activity is not a Create activity.
+ return TagConstructor.fromIdentifier(identifier);
+ }
+ })();
+ if (!tag) {
+ // ☡ Resolving the tag identifier failed.
+ throw new RangeError(
+ `Cannot apply activity: No tag for identifier: ${identifier}.`,
+ );
+ } else {
+ // Resolving the identifier succeeded; apply the changes to the
+ // tag and then silently persist it.
+ for (
+ const [statements, mode] of [
+ [unstates ?? [], "delete"],
+ [states ?? [], "add"],
+ ]
+ ) {
+ // Delete unstatements, then add statements.
+ for (const { predicate: $p, object: $o } of statements) {
+ // Iterate over the statements and apply them.
+ const predicate = `${$p}`;
+ const term = predicate in dataProperties
+ ? langString($o)
+ : predicate in objectProperties &&
+ !(predicate in transitiveProperties ||
+ objectProperties[predicate].inverseOf != null)
+ ? `${$o}`
+ : null;
+ if (term == null) {
+ // The provided predicate is not recognized; ignore it.
+ /* do nothing */
+ } else if (predicate == "prefLabel") {
+ // Preflabels are handled specially.
+ if (mode == "delete") {
+ // Unstating a preflabel has no effect unless a new one
+ // is also stated.
+ /* do nothing */
+ } else {
+ // Update the preflabel.
+ tag.prefLabel = term;
+ }
+ } else {
+ // The predicate is not `"prefLabel"`.
+ const related = (() => {
+ // If the predicate is an object property, attempt to
+ // resolve the object.
+ if (!(predicate in objectProperties)) {
+ // The predicate is not an object property; return
+ // null.
+ return null;
+ } else {
+ // The predicate is an object property.
+ try {
+ // Attempt to resolve the object.
+ return TagConstructor.fromIRI(term);
+ } catch {
+ // Resolving failed; return undefined.
+ return undefined;
+ }
+ }
+ })();
+ if (related === undefined) {
+ // The predicate is an object property, but its object
+ // was not resolvable.
+ //
+ // ☡ This is a silent error to allow for selective
+ // replay of activities while ignoring terms which are
+ // not covered.
+ /* do nothing */
+ } else {
+ // The predicate is not an object property or has a
+ // resolvable object.
+ //
+ // Apply the statement.
+ tag[
+ mode.concat(
+ predicate[0].toUpperCase(),
+ predicate.substring(1),
+ predicate in objectProperties ? "Tag" : "",
+ )
+ ](related ?? term);
+ }
+ }
+ }
+ }
+ tag.persist(true);
+ return tag;
+ }
}
}