X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/e20983aca1b36dd368d52e02ee018c2eaf0ca55f..91b9ab704dc286cc5e5a5d9cd6e3f48313d2c9f2:/model.js?ds=sidebyside diff --git a/model.js b/model.js index 3f0b6ad..3c8287e 100644 --- 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 . +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,136 +416,46 @@ 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, - }]), - ]), - ); + 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; } } @@ -533,53 +465,15 @@ class Tag { * ※ 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 @@ -624,6 +518,15 @@ class Tag { 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 @@ -930,6 +833,232 @@ 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: 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 @@ -1396,6 +1525,9 @@ export class TagSystem { /** The identifier of this `TagSystem`. */ #identifier; + /** The schema used by this `TagSystem. */ + #schema = schema; + /** The internal `Storage` of this `TagSystem`. */ #storage = new Storage(); @@ -1449,7 +1581,166 @@ export class TagSystem { } 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; + } } }