return tag;
}
+ /**
+ * Returns a new `Tag` with the provided identifier, kind, and
+ * prefLabel.
+ *
+ * ※ This function exists to enable `TagSystem`s to replay Create
+ * activities, maintaining the identifier of the original.
+ *
+ * ☡ This function throws if the provided identifier is already in
+ * use.
+ *
+ * ※ This function is not exposed.
+ */
+ static new(system, identifier, kind = "Tag", prefLabel = "") {
+ const storage = (new system.Tag()).#storage;
+ if (storage.has(identifier)) {
+ throw new RangeError(
+ `Cannot create Tag: Identifier already in use: ${identifier}.`,
+ );
+ } else {
+ const createdTag = new system.Tag(kind, prefLabel);
+ createdTag.#identifier = identifier;
+ createdTag.persist(true);
+ return createdTag;
+ }
+ }
+
/**
* Returns the `TagSystem` that the provided value belongs to.
*
/** 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;
+ }
}
}
// `::[Storage.toObject]` is tested by `::persist`.
});
+ describe("::apply", () => {
+ let system;
+ let Tag;
+
+ beforeEach(() => {
+ system = new TagSystem("example", "1972-12-31");
+ Tag = system.Tag;
+ });
+
+ it("[[Call]] throws if no activity is provided", () => {
+ assertThrows(() => {
+ system.apply();
+ });
+ });
+
+ it("[[Call]] throws with an invalid activity", () => {
+ assertThrows(() => {
+ system.apply({});
+ });
+ });
+
+ it("[[Call]] throws when specifying an invalid object", () => {
+ assertThrows(() => {
+ system.apply({
+ object: "",
+ });
+ });
+ assertThrows(() => {
+ system.apply({
+ object: `${system.iriSpace}000-0000`,
+ });
+ });
+ });
+
+ it("[[Call]] returns the tag being modified", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const applied = system.apply({ object: tag.iri });
+ assertStrictEquals(
+ Object.getPrototypeOf(applied),
+ system.Tag.prototype,
+ );
+ assertStrictEquals(tag.identifier, applied.identifier);
+ });
+
+ it("[[Call]] applies the changes", () => {
+ const broaderTag = new Tag();
+ const broaderActivity = broaderTag.persist();
+ const otherBroaderTag = new Tag();
+ const otherBroaderActivity = otherBroaderTag.persist();
+ const tag = new Tag("EntityTag", "my pref label");
+ tag.addHiddenLabel("label");
+ tag.addBroaderTag(broaderTag);
+ const createActivity = tag.persist();
+ tag.prefLabel = "new pref label";
+ tag.addAltLabel("alternative label");
+ tag.deleteHiddenLabel("label");
+ tag.addBroaderTag(otherBroaderTag);
+ tag.deleteBroaderTag(broaderTag);
+ const updateActivity = tag.persist();
+ const otherSystem = new TagSystem(
+ system.authorityName,
+ system.date,
+ );
+ otherSystem.apply(broaderActivity);
+ otherSystem.apply(otherBroaderActivity);
+ const appliedCreate = otherSystem.apply(createActivity);
+ assertStrictEquals(appliedCreate.kind, "EntityTag");
+ assertStrictEquals(appliedCreate.identifier, tag.identifier);
+ assertEquals(
+ { ...appliedCreate.prefLabel },
+ { "@value": "my pref label" },
+ );
+ assertEquals(
+ [...appliedCreate.hiddenLabels()].map(($) => ({ ...$ })),
+ [{ "@value": "label" }],
+ );
+ assertEquals(
+ [...appliedCreate.broaderTags()].map(($) => $.identifier),
+ [broaderTag.identifier],
+ );
+ const appliedUpdate = otherSystem.apply(updateActivity);
+ assertEquals(
+ { ...appliedUpdate.prefLabel },
+ { "@value": "new pref label" },
+ );
+ assertEquals(
+ [...appliedUpdate.altLabels()].map(($) => ({ ...$ })),
+ [{ "@value": "alternative label" }],
+ );
+ assertEquals([...appliedUpdate.hiddenLabels()], []);
+ assertEquals(
+ [...appliedUpdate.broaderTags()].map(($) => $.identifier),
+ [otherBroaderTag.identifier],
+ );
+ });
+
+ it("[[Call]] silently fails deleting preflabels", () => {
+ const tag = new system.Tag("Tag", "my pref label");
+ tag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ unstates: [{
+ predicate: "prefLabel",
+ object: "my pref label",
+ }],
+ });
+ assertEquals(
+ { ...applied.prefLabel },
+ { "@value": "my pref label" },
+ );
+ });
+
+ it("[[Call]] silently fails deleting unrecognized statements", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const otherTag = new Tag();
+ otherTag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ unstates: [{
+ predicate: "bad_statement",
+ object: otherTag.iri,
+ }],
+ });
+ assert(applied);
+ });
+
+ it("[[Call]] silently fails deleting immutable statements", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ unstates: [{ predicate: "a", object: "Tag" }],
+ });
+ assertStrictEquals(applied.kind, "Tag");
+ });
+
+ it("[[Call]] silently fails deleting inverse statements", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const otherTag = new Tag();
+ otherTag.addBroaderTag(tag);
+ otherTag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ unstates: [{ predicate: "narrower", object: otherTag.iri }],
+ });
+ assertStrictEquals(
+ [...applied.narrowerTags()][0].identifier,
+ otherTag.identifier,
+ );
+ });
+
+ it("[[Call]] sets preflabels", () => {
+ const tag = new Tag("Tag", "my pref label");
+ tag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ states: [{ predicate: "prefLabel", object: "new pref label" }],
+ });
+ assertEquals(
+ { ...applied.prefLabel },
+ { "@value": "new pref label" },
+ );
+ });
+
+ it("[[Call]] silently fails setting unrecognized statements", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const otherTag = new Tag();
+ otherTag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ states: [{ predicate: "bad_statement", object: otherTag.iri }],
+ });
+ assert(applied);
+ });
+
+ it("[[Call]] silently fails setting immutable statements", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ states: [{ predicate: "a", object: "RelationshipTag" }],
+ });
+ assertStrictEquals(applied.kind, "Tag");
+ });
+
+ it("[[Call]] silently fails setting inverse statements", () => {
+ const tag = new Tag();
+ tag.persist(true);
+ const otherTag = new Tag();
+ otherTag.persist(true);
+ const applied = system.apply({
+ object: tag.iri,
+ unstates: [{ predicate: "narrower", object: otherTag.iri }],
+ });
+ assertEquals([...applied.narrowerTags()], []);
+ });
+ });
+
describe("::authorityName", () => {
it("[[Get]] returns the authority name", () => {
const system = new TagSystem("etaoin.example", "1972-12-31");