X-Git-Url: https://git.ladys.computer/Etiquette/blobdiff_plain/998a27ffd305bdbef26b0f864924660e575e6e44..a3aca34a22550e1555bcdf8a571492213bfa83b0:/model.js?ds=inline diff --git a/model.js b/model.js index 6963a16..d64f6c4 100644 --- a/model.js +++ b/model.js @@ -7,120 +7,10 @@ // 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"; - -// TODO: Move these somewhere else and allow for modification before -// they are used, freezing them only once tags actually start being -// constructed (probably on first call to the `TagSystem` initializer -// for convenience). -// -// Or else make them properties of the tag system itself and ∼fully -// modifiable. - -/** - * Tag kinds which denote entity tags. - * - * ※ This object is not exposed. - */ -const ENTITY_TAG_KINDS = new Set([ - "EntityTag", - "CharacterTag", - "InanimateEntityTag", -]); - -/** - * Tag kinds which denote relationship tags. - * - * ※ This object is not exposed. - */ -const RELATIONSHIP_TAG_KINDS = new Set([ - "RelationshipTag", - "FamilialRelationship Tag", - "FriendshipTag", - "RivalryTag", - "RomanticRelationshipTag", - "SexualRelationshipTag", -]); - -/** - * Tag kinds which denote setting tags. - * - * ※ This object is not exposed. - */ -const SETTING_TAG_KINDS = new Set([ - "SettingTag", - "LocationTag", - "TimePeriodTag", - "UniverseTag", -]); - -/** - * Tag kinds which denote conceptual tags. - * - * ※ This object is not exposed. - */ -const CONCEPTUAL_TAG_KINDS = new Set(function* () { - yield "ConceptualTag"; - yield* RELATIONSHIP_TAG_KINDS; -}()); - -/** - * All recognized tag kinds. - * - * ※ This object is not exposed. - */ -const TAG_KINDS = new Set(function* () { - yield "Tag"; - yield "CanonTag"; - yield* CONCEPTUAL_TAG_KINDS; - yield* ENTITY_TAG_KINDS; - yield "GenreTag"; - yield* SETTING_TAG_KINDS; -}()); - -/** - * Tag kinds which can be in canon. - * - * ※ This object is not exposed. - */ -const HAS_IN_CANON = new Set(function* () { - yield* ENTITY_TAG_KINDS; - yield* SETTING_TAG_KINDS; -}()); - -/** - * Tag kinds which can be involved in relationship tags. - * - * ※ This object is not exposed. - */ -const INVOLVABLE_IN_RELATIONSHIP = new Set(function* () { - yield "CharacterTag"; - yield* RELATIONSHIP_TAG_KINDS; -}()); - -/** - * Properties which take literal values instead of identifiers. - * - * These are the label terms. - */ -const LITERAL_TERMS = new Set([ - "prefLabel", - "altLabel", - "hiddenLabel", -]); - -/** - * Properties to skip when diffing. - * - * These are all inverses of properties included in diffs and cannot be - * changed manually. - */ -const SKIP_IN_DIFF = new Set([ - "hasInCanon", - "isIncludedIn", - "narrower", -]); +import schema from "./schema.js"; /** * A tag. @@ -143,6 +33,9 @@ class Tag { /** The `Storage` managed by this `Tag`’s `TagSystem`. */ #storage; + /** The schema in use for this `Tag`. */ + #schema; + /** * The 30‐bit W·R·M·G base32 identifier with leading checksum which * has been assigned to this `Tag`. @@ -167,314 +60,73 @@ class Tag { #data = tagData(); /** - * Returns whether or not the provided value is a tag which shares a - * storage with this tag. - * - * Sharing a storage also implies sharing a `TagSystem`. - */ - #isTagInStorage($) { - try { - // Try to compare the provided value’s internal store with - // the provided storage. - return $.#storage == this.#storage; - } catch { - // The provided value was not a `Tag`. - return false; - } - } - - /** - * Constructs a new `Tag` of the provided kind and with the provided - * preferred label. - * - * ※ The first two arguments of this constructor are bound when - * generating the value of `TagSystem::Tag`. It isn’t possible to - * access this constructor in its unbound form from outside this - * module. - * - * ☡ This constructor throws if the provided kind is not supported. - */ - constructor(system, storage, kind = "Tag", prefLabel = "") { - const kindString = `${kind}`; - if (TAG_KINDS.has(kindString)) { - // The provided kind is one of the recognized tag kinds. - this.#system = system; - this.#storage = storage; - this.#kind = kindString; - this.#data.prefLabel = prefLabel; - } else { - // The provided kind is not supported. - throw new RangeError( - `Cannot construct Tag: Unrecognized kind: ${kind}.`, - ); - } - } - - /** - * 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. - * - * ※ If the I·R·I is not recognized, this function returns `null`. - */ - static fromIRI(system, storage, iri) { - const name = `${iri}`; - const prefix = - `https://${system.authorityName}/tag:${system.taggingEntity}:`; - if (!name.startsWith(prefix)) { - // The I·R·I does not begin with the expected prefix. - return null; - } 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; - } - - /** - * 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. - * - * ☡ 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`. - */ - 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: ${tagName}`, - ); - } 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; - } - } - } - - /** - * Returns the `TagSystem` that the provided value belongs to. - * - * ※ This function can be used to check if the provided value has - * private tag features. - * - * ※ 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. + * Adds the provided label(s) to this `Tag` as the provided + * predicate, then returns this `Tag`. */ - 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 - // `Tag`. - Object.defineProperties(this.prototype, { - constructor: { - configurable: true, - enumerable: false, - get() { - // All `Tag`s are constructed via the `.Tag` constructor - // available in their `TagSystem`; return it. - return this.#system.Tag; - }, - set: undefined, - }, - }); - } - - /** - * Adds the provided label(s) to this `Tag` as alternate labels, then - * returns this `Tag`. - */ - addAltLabel(...labels) { - const altLabels = this.#data.altLabel; - let objectLabels = null; // initialized on first use + #addLabel(predicate, ...labels) { + const values = this.#data[predicate]; for (const $ of labels) { // Iterate over each provided label and attempt to add it. const literal = langString($); - if (Object(literal) === literal) { - // The current label is a language‐tagged string. - objectLabels ??= [...function* () { - for (const altLabel of altLabels) { - // Iterate over the existing labels and yield the - // language‐tagged strings. - if (Object(altLabel) === altLabel) { - // The current existing label is a language‐tagged - // string. - yield altLabel; - } else { - // The current existing label is not a language‐tagged - // string. - /* do nothing */ - } - } - }()]; - if ( - objectLabels.some((objectLabel) => - objectLabel["@value"] == literal["@value"] && - objectLabel["@language"] == literal["@language"] - ) - ) { - // There is a match with the current label in the existing - // labels. - /* do nothing */ - } else { - // There is no match and this label must be added. - altLabels.add(literal); - objectLabels.push(literal); - } - } else { - // The current label is a simple string. - altLabels.add(literal); - } + values.add(literal); } return this; } /** * Adds the provided tags to the list of tags that this `Tag` is - * narrower than, then returns this `Tag`. + * related to by the provided predicate, then returns this `Tag`. * * Arguments may be string identifiers or objects with an * `.identifier` property. */ - addBroaderTag(...tags) { + #addTag(predicate, ...tags) { const storage = this.#storage; - const broader = this.#data.broader; + const values = this.#data[predicate]; for (const $ of tags) { - // Iterate over each tag and attempt to set it as broader than - // this `Tag`. + // Iterate over each tag and attempt to state the predicate. const identifier = toIdentifier($); if (identifier == null) { // ☡ The current tag has no identifier. throw new TypeError( - "Cannot assign broader to Tag: Identifier must not be nullish.", + `Cannot state ${predicate} of Tag: Identifier must not be nullish.`, ); - } else if (broader.has(identifier)) { - // Short‐circuit: The identifier is already something this - // `Tag` is narrower than. + } else if (values.has(identifier)) { + // Short‐circuit: The identifier has already been stated with + // this predicate. /* do nothing */ } else { - // The current tag has an identifier. + // The current tag has an identifier, but it hasn’t been stated + // with this predicate yet. const tag = storage.get(identifier); if (tag == null) { // ☡ The current tag has not been persisted to this `Tag`’s // storage. throw new RangeError( - `Cannot assign broader to Tag: Identifier is not persisted: ${identifier}.`, + `Cannot state ${predicate} of Tag: Identifier is not persisted: ${identifier}.`, ); } else if (!this.#isTagInStorage(tag)) { // ☡ The current tag is not a tag in the correct tag system. throw new TypeError( - `Cannot assign broader to Tag: Tags must be from the same Tag System, but got: ${identifier}.`, + `Cannot state ${predicate} of Tag: Tags must be from the same Tag System, but got: ${identifier}.`, + ); + } else if ( + !isObjectPredicateOK( + this.#schema, + this.#kind, + predicate, + tag.#kind, + ) + ) { + // ☡ This tag and the current tag form an invalid pair for + // this predicate. + throw new TypeError( + `Cannot state ${predicate} of Tag: Not valid for domain and range: ${this.#kind}, ${tag.#kind}.`, ); } else { // The current tag is a tag in the correct tag system; add // its identifier. - broader.add(identifier); + values.add(identifier); } } } @@ -482,253 +134,134 @@ class Tag { } /** - * Adds the provided label(s) to this `Tag` as hidden labels, then - * returns this `Tag`. + * Removes the provided string label(s) from this `Tag` as the + * provided predicate, then returns this `Tag`. */ - addHiddenLabel(...labels) { - const hiddenLabels = this.#data.hiddenLabel; - let objectLabels = null; // initialized on first use + #deleteLabel(predicate, ...labels) { + const values = this.#data[predicate]; for (const $ of labels) { - // Iterate over each provided label and attempt to add it. + // Iterate over each provided label and attempt to remove it. const literal = langString($); - if (Object(literal) === literal) { - // The current label is a language‐tagged string. - objectLabels ??= [...function* () { - for (const hiddenLabel of hiddenLabels) { - // Iterate over the existing labels and yield the - // language‐tagged strings. - if (Object(hiddenLabel) === hiddenLabel) { - // The current existing label is a language‐tagged - // string. - yield hiddenLabel; - } else { - // The current existing label is not a language‐tagged - // string. - /* do nothing */ - } - } - }()]; - if ( - objectLabels.some((objectLabel) => - objectLabel["@value"] == literal["@value"] && - objectLabel["@language"] == literal["@language"] - ) - ) { - // There is a match with the current label in the existing - // labels. - /* do nothing */ - } else { - // There is no match and this label must be added. - hiddenLabels.add(literal); - objectLabels.push(literal); - } - } else { - // The current label is a simple string. - hiddenLabels.add(literal); - } + values.delete(literal); } return this; } /** - * Adds the provided tags to the list of tags that this `Tag` is in - * canon with, then returns this `Tag`. + * Removes the provided tags from the list of tags that this `Tag` is + * related to by the provided predicate, then returns this `Tag`. * * Arguments may be string identifiers or objects with an * `.identifier` property. - * - * ☡ This method will throw if a provided argument does not indicate - * a canon tag, or if this `Tag` is not of a kind which can be placed - * in canon. */ - addInCanonTag(...tags) { - const storage = this.#storage; - const kind = this.#kind; - const inCanon = this.#data.inCanon; - if (!HAS_IN_CANON.has(kind)) { - // ☡ This is not an entity tag, setting tag, or recognized - // subclass. - throw new TypeError( - `Cannot put Tag in canon: Incorrect Tag type: ${kind}.`, - ); - } else { - // This has a kind which can be placed in canon. - for (const $ of tags) { - // Iterate over each tag and attempt to set this `Tag` in canon - // of it. - const identifier = toIdentifier($); - if (identifier == null) { - // ☡ The current tag has no identifier. - throw new TypeError( - "Cannot put Tag in canon: Identifier must not be nullish.", - ); - } else if (inCanon.has(identifier)) { - // Short‐circuit: The identifier is already something this - // `Tag` is in canon of. - /* do nothing */ - } else { - // The current tag has an identifier. - const tag = storage.get(identifier); - if (tag == null) { - // ☡ The current tag has not been persisted to this `Tag`’s - // storage. - throw new RangeError( - `Cannot put Tag in canon: Identifier is not persisted: ${identifier}.`, - ); - } else if ( - // ※ If the first check succeeds, then the current tag - // must have `Tag` private class features. - !this.#isTagInStorage(tag) || tag.#kind != "CanonTag" - ) { - // ☡ The current tag is not a canon tag in the correct - // tag system. - throw new TypeError( - `Cannot put Tag in canon: Tags can only be in Canon Tags from the same Tag System, but got: ${identifier}.`, - ); - } else { - // The current tag is a canon tag in the correct tag - // system; add its identifier. - inCanon.add(identifier); - } - } - } + #deleteTag(predicate, ...tags) { + const values = this.#data[predicate]; + for (const $ of tags) { + // Iterate over the provided tags and delete them. + values.delete(toIdentifier($)); } return this; } /** - * Adds the provided tags to the list of tags that this `Tag` - * involves, then returns this `Tag`. - * - * Arguments may be string identifiers or objects with an - * `.identifier` property. + * Returns whether or not the provided value is a tag which shares a + * storage with this tag. * - * ☡ This method will throw if this `Tag` is not a conceptual tag, or - * if this `Tag` is a relationship tag and a provided argument does - * not indicate a character or relationship tag. + * Sharing a storage also implies sharing a `TagSystem`. */ - addInvolvesTag(...tags) { - const storage = this.#storage; - const kind = this.#kind; - const involves = this.#data.involves; - if (!CONCEPTUAL_TAG_KINDS.has(kind)) { - // ☡ This is not a conceptual tag or recognized subclass. - throw new TypeError( - `Cannot involve Tag: Incorrect Tag type: ${kind}.`, - ); - } else { - // This is a conceptual tag. - for (const $ of tags) { - // Iterate over each tag and attempt to set this `Tag` as - // involving it. - const identifier = toIdentifier($); - if (identifier == null) { - // ☡ The current tag has no identifier. - throw new TypeError( - "Cannot involve Tag: Identifier must not be nullish.", - ); - } else if (involves.has(identifier)) { - // Short‐circuit: The identifier is already something this - // `Tag` involves. - /* do nothing */ - } else { - // The current tag has an identifier. - const tag = storage.get(identifier); - if (tag == null) { - // ☡ The current tag has not been persisted to this `Tag`’s - // storage. - throw new RangeError( - `Cannot involve Tag: Identifier is not persisted: ${identifier}.`, - ); - } else if ( - // ※ If the first check succeeds, then the current tag - // must have `Tag` private class features. - !this.#isTagInStorage(tag) || - RELATIONSHIP_TAG_KINDS.has(kind) && - !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind) - ) { - // ☡ The current tag is in the correct tag system and - // includable. - throw new TypeError( - `Cannot involve Tag: Tags must be the same Tag System and involvable, but got: ${identifier}.`, - ); - } else { - // The current tag is an involvable tag in the correct tag - // system; add its identifier. - involves.add(identifier); - } - } - } + #isTagInStorage($) { + try { + // Try to compare the provided value’s internal store with + // the provided storage. + return $.#storage == this.#storage; + } catch { + // The provided value was not a `Tag`. + return false; } - return this; } - /** Yields the alternative labels of this `Tag`. */ - *altLabels() { - yield* this.#data.altLabel; - } - - /** Returns the authority (domain) name for this `Tag`. */ - get authorityName() { - return this.#system.authorityName; + /** + * Yields the labels of this `Tag` according to the provided + * predicate. + */ + *#yieldLabels(predicate) { + yield* this.#data[predicate]; } - /** Yields `Tag`s which are broader than this `Tag`. */ - *broaderTags() { + /** + * Yields the tags that this `Tag` is related to by the provided + * predicate. + */ + *#yieldTags(predicate) { const storage = this.#storage; - for (const identifier of this.#data.broader) { - // Iterate over the broader tags and yield them if possible. + for (const identifier of this.#data[predicate]) { + // Iterate over the tags in this predicate and yield them if + // possible. const tag = storage.get(identifier); - if (!this.#isTagInStorage(tag)) { - // The broader tag no longer appears in storage; perhaps it was - // deleted. + if ( + !this.#isTagInStorage(tag) || !isObjectPredicateOK( + this.#schema, + this.#kind, + predicate, + tag.#kind, + ) + ) { + // The tag no longer appears in storage or is not compatible; + // perhaps it was deleted. /* do nothing */ } else { - // The broader tag exists and is constructable from storage. + // The tag exists and is constructable from storage. yield tag; } } } - /** Yields `Tag`s which are broader than this `Tag`, transitively. */ - *broaderTransitiveTags() { + /** + * Yields the tags that this `Tag` is related to by the provided + * predicate, figured transitively. + */ + *#yieldTransitiveTags(transitivePredicate, basePredicate) { const storage = this.#storage; const encountered = new Set(); - let pending = new Set(this.#data.broader); + let pending = new Set(this.#data[basePredicate]); while (pending.size > 0) { - // Loop until all broader tags have been encountered. + // Loop until all tags of the predicate have been encountered. const processing = pending; pending = new Set(); for (const identifier of processing) { - // Iterate over the broader tags and yield them if possible. + // Iterate over the tags and yield them if possible. if (!encountered.has(identifier)) { - // The broader tag has not been encountered before. + // The tag has not been encountered before. encountered.add(identifier); const tag = storage.get(identifier); - if (!this.#isTagInStorage(tag)) { - // The broader tag no longer appears in storage; perhaps it - // was deleted. + if ( + !this.#isTagInStorage(tag) || !isObjectPredicateOK( + this.#schema, + this.#kind, + transitivePredicate, + tag.#kind, + ) + ) { + // The tag no longer appears in storage or is not + // compatible; perhaps it was deleted. /* do nothing */ } else { - // The broader tag exists and is constructable from - // storage. + // The tag exists and is constructable from storage. yield tag; - for (const transitive of tag.#data.broader) { - // Iterate over the broader tags of the current broader - // tag and add them to pending as needed. + for (const transitive of tag.#data[basePredicate]) { + // Iterate over the nested tags of the current tag and + // add them to pending as needed. if (!encountered.has(transitive)) { - // The broader broader tag has not been encountered - // yet. + // The nested tag has not been encountered yet. pending.add(transitive); } else { - // The broader broader tag has already been - // encountered. + // The nested tag has already been encountered. /* do nothing */ } } } } else { - // The broader tag has already been encountered. + // The tag has already been encountered. /* do nothing */ } } @@ -736,160 +269,203 @@ class Tag { } /** - * Removes the provided string label(s) from this `Tag` as alternate - * labels, then returns this `Tag`. - */ - deleteAltLabel(...labels) { - const altLabels = this.#data.altLabel; - let objectLabels = null; // initialized on first use - for (const $ of labels) { - // Iterate over each provided label and attempt to remove it. - const literal = langString($); - if (Object(literal) === literal) { - // The current label is a language‐tagged string. - objectLabels ??= [...function* () { - for (const altLabel of altLabels) { - // Iterate over the existing labels and yield the - // language‐tagged strings. - if (Object(altLabel) === altLabel) { - // The current existing label is a language‐tagged - // string. - yield altLabel; - } else { - // The current existing label is not a language‐tagged - // string. - /* do nothing */ - } - } - }()]; - const existing = objectLabels.find((objectLabel) => - objectLabel["@value"] == literal["@value"] && - objectLabel["@language"] == literal["@language"] - ); - altLabels.delete(existing); - } else { - // The current label is a simple string. - altLabels.delete(literal); - } - } - return this; - } - - /** - * Removes the provided tags from the list of tags that this `Tag` is - * narrower than, then returns this `Tag`. + * Constructs a new `Tag` of the provided kind and with the provided + * preferred label. * - * Arguments may be string identifiers or objects with an - * `.identifier` property. + * ※ The first two arguments of this constructor are bound when + * generating the value of `TagSystem::Tag`. It isn’t possible to + * access this constructor in its unbound form from outside this + * module. + * + * ☡ This constructor throws if the provided kind is not supported. */ - deleteBroaderTag(...tags) { - const broader = this.#data.broader; - for (const $ of tags) { - // Iterate over the provided tags and delete them. - broader.delete(toIdentifier($)); + constructor(system, storage, schema, kind = "Tag", prefLabel = "") { + this.#system = system; + this.#storage = storage; + this.#schema = schema; + const kindString = `${kind}`; + if (!(kindString in schema.classes)) { + // The provided kind is not supported. + throw new RangeError( + `Cannot construct Tag: Unrecognized kind: ${kind}.`, + ); + } else { + // The provided kind is one of the recognized tag kinds. + this.#kind = kindString; + this.#data.prefLabel = prefLabel; } - return this; } /** - * Removes the provided string label(s) from this `Tag` as hidden - * labels, then returns this `Tag`. + * Returns a new `Tag` constructor for the provided system, storage, + * schema, created with an appropriate prototype for the properties + * so defined. + * + * ※ This function is not exposed. */ - deleteHiddenLabel(...labels) { - const hiddenLabels = this.#data.hiddenLabel; - let objectLabels = null; // initialized on first use - for (const $ of labels) { - // Iterate over each provided label and attempt to remove it. - const literal = langString($); - if (Object(literal) === literal) { - // The current label is a language‐tagged string. - objectLabels ??= [...function* () { - for (const hiddenLabel of hiddenLabels) { - // Iterate over the existing labels and yield the - // language‐tagged strings. - if (Object(hiddenLabel) === hiddenLabel) { - // The current existing label is a language‐tagged - // string. - yield hiddenLabel; - } else { - // The current existing label is not a language‐tagged - // string. - /* do nothing */ - } - } - }()]; - const existing = objectLabels.find((objectLabel) => - objectLabel["@value"] == literal["@value"] && - objectLabel["@language"] == literal["@language"] - ); - hiddenLabels.delete(existing); - } else { - // The current label is a simple string. - hiddenLabels.delete(literal); - } - } - return this; + static For(system, storage, schema) { + const { + objectProperties, + transitiveProperties, + dataProperties, + } = schema; + const constructor = function (...$s) { + return Reflect.construct( + Tag, + [system, storage, schema, ...$s], + new.target, + ); + }; + Object.defineProperties(constructor, { + name: { value: "TagSystem::Tag" }, + prototype: { + configurable: false, + enumerable: false, + value: Object.create( + Tag.prototype, + 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.#addTag(key, ...tags); + }]; + yield [`delete${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, + key, + ); + }]; + } 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* () { + yield* this.#yieldLabels(key); + }]; + yield [`add${cased}`, function (...labels) { + return this.#addLabel(key, ...labels); + }]; + yield [`delete${cased}`, function (...labels) { + 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 */ + } + } + }(), + ([key, value]) => [key, { + configurable: true, + enumerable: false, + value: Object.defineProperty(value, "name", { + value: key, + }), + writable: true, + }], + )), + ), + writable: false, + }, + }); + return new TagConstructor(constructor, system, storage, schema); } /** - * Removes the provided tags from the list of tags that this `Tag` is - * in canon with, then returns this `Tag`. + * Assigns the provided data and identifier to the provided tag. * - * Arguments may be string identifiers or objects with an - * `.identifier` property. + * ☡ This function throws if the provided tag is not a `Tag`. + * + * ※ This function is not exposed. */ - deleteInCanonTag(...tags) { - const inCanon = this.#data.inCanon; - for (const $ of tags) { - // Iterate over the provided tags and delete them. - inCanon.delete(toIdentifier($)); - } - return this; + static assignData(tag, data, identifier) { + tag.#identifier = `${identifier}`; + tag.#persistedData = tagData(data); + tag.#data = tagData(data); + return tag; } /** - * Removes the provided tags from the list of tags that this `Tag` - * involves, then returns this `Tag`. + * Returns the `TagSystem` that the provided value belongs to. * - * Arguments may be string identifiers or objects with an - * `.identifier` property. + * ※ This function can be used to check if the provided value has + * private tag features. + * + * ※ This function is not exposed. */ - deleteInvolvesTag(...tags) { - const involves = this.#data.involves; - for (const $ of tags) { - // Iterate over the provided tags and delete them. - involves.delete(toIdentifier($)); - } - return this; + static getSystem($) { + return !(#system in Object($)) ? null : $.#system; } - /** Yields `Tag`s that are in canon of this `Tag`. */ - *hasInCanonTags() { - const storage = this.#storage; - if (this.#kind == "CanonTag") { - // This is a canon tag. - for (const identifier of this.#data.hasInCanon) { - // Iterate over the tags in canon and yield them if possible. - const tag = storage.get(identifier); - if ( - !this.#isTagInStorage(tag) || !HAS_IN_CANON.has(tag.#kind) - ) { - // The tag in canon no longer appears in storage; perhaps it - // was deleted. - /* do nothing */ - } else { - // The tag in canon exists and is constructable from storage. - yield tag; - } - } - } else { - /* do nothing */ - } + static { + // Overwrite the default `::constructor` method to instead give the + // actual (bound) constructor which was used to generate a given + // `Tag`. + Object.defineProperties(this.prototype, { + constructor: { + configurable: true, + enumerable: false, + get() { + // All `Tag`s are constructed via the `.Tag` constructor + // available in their `TagSystem`; return it. + return this.#system.Tag; + }, + set: undefined, + }, + }); } - /** Yields the hidden labels of this `Tag`. */ - *hiddenLabels() { - yield* this.#data.hiddenLabel; + /** Returns the authority (domain) name for this `Tag`. */ + get authorityName() { + return this.#system.authorityName; } /** Returns the identifier of this `Tag`. */ @@ -897,86 +473,15 @@ class Tag { return this.#identifier; } - /** Yields `Tag`s that this `Tag` is in canon of. */ - *inCanonTags() { - const storage = this.#storage; - if (HAS_IN_CANON.has(this.#kind)) { - // This tag can be placed in canon. - for (const identifier of this.#data.inCanon) { - // Iterate over the canon tags and yield them if possible. - const tag = storage.get(identifier); - if (!this.#isTagInStorage(tag) || tag.#kind != "CanonTag") { - // The canon tag no longer appears in storage; perhaps it was - // deleted. - /* do nothing */ - } else { - // The canon tag exists and is constructable from storage. - yield tag; - } - } - } else { - // This tag cannot be placed in canon. - /* do nothing */ - } - } - - /** Yields `Tag`s which involve this `Tag`. */ - *involvedInTags() { - const storage = this.#storage; - for (const identifier of this.#data.involvedIn) { - // Iterate over the involving tags and yield them if possible. - const tag = storage.get(identifier); - const tagKind = tag.#kind; - if ( - !this.#isTagInStorage(tag) || - !CONCEPTUAL_TAG_KINDS.has(tagKind) || - RELATIONSHIP_TAG_KINDS.has(tagKind) && - !INVOLVABLE_IN_RELATIONSHIP.has(this.#kind) - ) { - // The including tag no longer appears in storage; perhaps it - // was deleted. - /* do nothing */ - } else { - // The including tag exists and is constructable from storage. - yield tag; - } - } - } - - /** Yields `Tag`s that this `Tag` involves. */ - *involvesTags() { - const storage = this.#storage; - const kind = this.#kind; - if (CONCEPTUAL_TAG_KINDS.has(kind)) { - // This tag can involve other tags. - for (const identifier of this.#data.involves) { - // Iterate over the involved and yield them if possible. - const tag = storage.get(identifier); - if ( - !this.#isTagInStorage(tag) || - RELATIONSHIP_TAG_KINDS.has(kind) && - !INVOLVABLE_IN_RELATIONSHIP.has(tag.#kind) - ) { - // The involved tag no longer appears in storage; perhaps it - // was deleted. - /* do nothing */ - } else { - // The involved tag exists and is constructable from storage. - yield tag; - } - } - } else { - // This tag cannot involve other tags. - /* do nothing */ - } - } - /** Returns the I·R·I for this `Tag`. */ get iri() { - const tagURI = this.tagURI; - return tagURI == null - ? null - : `https://${this.authorityName}/${tagURI}`; + const { identifier, iriSpace } = this; + return identifier == null ? null : `${iriSpace}${identifier}`; + } + + /** Returns the I·R·I space for this `Tag`. */ + get iriSpace() { + return this.#system.iriSpace; } /** Returns the kind of this `Tag`. */ @@ -984,69 +489,6 @@ class Tag { return this.#kind; } - /** Yields `Tag`s which are narrower than this `Tag`. */ - *narrowerTags() { - const storage = this.#storage; - for (const identifier of this.#data.narrower) { - const tag = storage.get(identifier); - if (!this.#isTagInStorage(tag)) { - // The narrower tag no longer appears in storage; perhaps it - // was deleted. - /* do nothing */ - } else { - // The narrower tag exists and is constructable from storage. - yield tag; - } - } - } - - /** - * Yields `Tag`s which are narrower than this `Tag`, transitively. - */ - *narrowerTransitiveTags() { - const storage = this.#storage; - const encountered = new Set(); - let pending = new Set(this.#data.narrower); - while (pending.size > 0) { - // Loop until all narrower tags have been encountered. - const processing = pending; - pending = new Set(); - for (const identifier of processing) { - // Iterate over the narrower tags and yield them if possible. - if (!encountered.has(identifier)) { - // The narrower tag has not been encountered before. - encountered.add(identifier); - const tag = storage.get(identifier); - if (!this.#isTagInStorage(tag)) { - // The narrower tag no longer appears in storage; perhaps - // it was deleted. - /* do nothing */ - } else { - // The narrower tag exists and is constructable from - // storage. - yield tag; - for (const transitive of tag.#data.narrower) { - // Iterate over the narrower tags of the current narrower - // tag and add them to pending as needed. - if (!encountered.has(transitive)) { - // The narrower narrower tag has not been encountered - // yet. - pending.add(transitive); - } else { - // The narrower narrower tag has already been - // encountered. - /* do nothing */ - } - } - } - } else { - // The narrower tag has already been encountered. - /* do nothing */ - } - } - } - } - /** * Persist this `Tag` to storage and return an ActivityStreams * serialization of a Tag Activity representing any changes, or @@ -1060,19 +502,27 @@ class Tag { * as broader than another causes the other tag to reciprocally be * marked as narrower. * - * ※ The inverse terms `hasInCanon`, `isIncludedIn`, and `narrower` - * will never appear in the predicates of generated activities. + * ※ Inverse object properties will never appear in the predicates + * of generated activities. */ persist(silent = false) { const system = this.#system; const storage = this.#storage; + const { + objectProperties, + transitiveProperties, + dataProperties, + } = this.#schema; const persistedData = this.#persistedData; const data = this.#data; const diffs = {}; for (const [key, value] of Object.entries(data)) { // Iterate over each entry of the tag data and create a diff // with the last persisted information. - if (SKIP_IN_DIFF.has(key) || silent && LITERAL_TERMS.has(key)) { + if ( + objectProperties[key]?.inverseOf != null || + silent && key in dataProperties + ) { // The current property is one which is skipped in diffs. // // In a silent persist, this includes any literal terms. @@ -1090,24 +540,13 @@ class Tag { }; } else if (value instanceof Set) { // The current property is set‐valued. - let values = null; // initialized on first use const oldValues = new Set(persisted); const newValues = new Set(value); for (const existing of persisted) { // Iterate over each persisted property and either remove // it from the list of new values or add it to the list of // removed ones. - // - // ※ Some special handling is required here for - // language‐tagged strings. - if ( - value.has(existing) || - Object(existing) === existing && - (values ??= [...value]).some(($) => - `${$}` == `${existing}` && - $.language == existing.language - ) - ) { + if (value.has(existing)) { // The value is in both the old and new version of the // data. oldValues.delete(existing); @@ -1117,10 +556,7 @@ class Tag { /* do nothing */ } } - diffs[key] = { - old: oldValues, - new: newValues, - }; + diffs[key] = { old: oldValues, new: newValues }; } else if ( `${value}` != `${persisted}` || value.language != persisted.language @@ -1133,10 +569,7 @@ class Tag { }; } else { // The current property did not change. - diffs[key] = { - old: new Set(), - new: new Set(), - }; + diffs[key] = { old: new Set(), new: new Set() }; } } } @@ -1152,38 +585,41 @@ class Tag { } const persistedIdentifier = this.#identifier; this.#persistedData = tagData(data); // cloning here is necessary - for ( - const [term, inverse] of [ - ["broader", "narrower"], - ["inCanon", "hasInCanon"], - ["involves", "involvedIn"], - ] - ) { - // Iterate over each term referencing other tags and update the - // inverse property on those tags if possible. - for (const referencedIdentifier of diffs[term].old) { - // Iterate over the removed tags and remove this `Tag` from - // their inverse property. - const referenced = storage.get(referencedIdentifier); - try { - // Try removing this `Tag`. - referenced.#data[inverse].delete(persistedIdentifier); - storage.set(referencedIdentifier, referenced); - } catch { - // Removal failed, possibly because the other tag was - // deleted. - /* do nothing */ + for (const inverse in objectProperties) { + // Iterate over each non‐transitive inverse property and update + // it based on its inverse on the corresponding tags if possible. + const term = objectProperties[inverse].inverseOf; + if (term == null || term in transitiveProperties) { + // The current property is not the inverse of an non‐transitive + // property. + /* do nothing */ + } else { + // The current property is the inverse of a non‐transitive + // property. + for (const referencedIdentifier of diffs[term].old) { + // Iterate over the removed tags and remove this `Tag` from + // their inverse property. + const referenced = storage.get(referencedIdentifier); + try { + // Try removing this `Tag`. + referenced.#data[inverse].delete(persistedIdentifier); + storage.set(referencedIdentifier, referenced); + } catch { + // Removal failed, possibly because the other tag was + // deleted. + /* do nothing */ + } } - } - for (const referencedIdentifier of diffs[term].new) { - const referenced = storage.get(referencedIdentifier); - try { - // Try adding this `Tag`. - referenced.#data[inverse].add(persistedIdentifier); - storage.set(referencedIdentifier, referenced); - } catch { - // Adding failed, possibly because the other tag was deleted. - /* do nothing */ + for (const referencedIdentifier of diffs[term].new) { + const referenced = storage.get(referencedIdentifier); + try { + // Try adding this `Tag`. + referenced.#data[inverse].add(persistedIdentifier); + storage.set(referencedIdentifier, referenced); + } catch { + // Adding failed, possibly because the other tag was deleted. + /* do nothing */ + } } } } @@ -1225,14 +661,11 @@ class Tag { // things as needed. for (const oldValue of oldValues) { // Iterate over removals and unstate them. - if (LITERAL_TERMS.has(term)) { - // This is a literal term; push the change wrapped in an - // object. + if (term in dataProperties) { + // This is a literal term; push it. unstates.push({ predicate: term, - object: Object(oldValue) === oldValue - ? { ...langString(oldValue) } - : { "@value": `${oldValue}` }, + object: { ...oldValue }, }); } else { // This is a named term; attempt to get its I·R·I and @@ -1259,14 +692,11 @@ class Tag { } for (const newValue of newValues) { // Iterate over additions and state them. - if (LITERAL_TERMS.has(term)) { - // This is a literal term; push the change wrapped in an - // object. + if (term in dataProperties) { + // This is a literal term; push it. states.push({ predicate: term, - object: Object(newValue) === newValue - ? { ...langString(newValue) } - : { "@value": `${newValue}` }, + object: { ...newValue }, }); } else { // This is a named term; attempt to get its I·R·I and @@ -1367,9 +797,448 @@ class Tag { const { /** - * Returns the provided value converted into either a plain string - * primitive or an object with `.["@value"]` and `.["@language"]` - * properties. + * 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 + * property, and object class are consistent. + * + * This is hardly a full reasoner; it is tuned to the abilites and + * needs of this module. + */ + isObjectPredicateOK, +} = (() => { + const cachedClassAndSuperclasses = new WeakMap(); + const cachedClassRestrictions = new WeakMap(); + const cachedPredicateRestrictions = new WeakMap(); + + const classAndSuperclasses = function* ( + classes, + baseClass, + touched = new Set(), + ) { + if (baseClass == "Thing" || touched.has(baseClass)) { + /* do nothing */ + } else { + yield baseClass; + touched.add(baseClass); + const subClassOf = classes[baseClass]?.subClassOf ?? "Thing"; + for ( + const superclass of ( + typeof subClassOf == "string" + ? [subClassOf] + : Array.from(subClassOf) + ).filter(($) => typeof $ == "string") + ) { + yield* classAndSuperclasses(classes, superclass, touched); + } + } + }; + + const getClassAndSuperclasses = (schema, baseClass) => { + const schemaCache = cachedClassAndSuperclasses.get(schema); + const cached = schemaCache?.[baseClass]; + if (cached != null) { + return cached; + } else { + const { classes } = schema; + const result = [...classAndSuperclasses(classes, baseClass)]; + if (schemaCache) { + schemaCache[baseClass] = result; + } else { + cachedClassRestrictions.set( + schema, + Object.assign(Object.create(null), { [baseClass]: result }), + ); + } + return result; + } + }; + + const getClassRestrictions = (schema, domain) => { + const schemaCache = cachedClassRestrictions.get(schema); + const cached = schemaCache?.[domain]; + if (cached != null) { + return cached; + } else { + const { classes } = schema; + const restrictions = Object.create(null); + const subClassOf = classes[domain]?.subClassOf ?? "Thing"; + for ( + const superclass of ( + typeof subClassOf == "string" + ? [subClassOf] + : Array.from(subClassOf) + ).filter(($) => Object($) === $) + ) { + const { onProperty, allValuesFrom } = superclass; + restrictions[onProperty] = processSpace(allValuesFrom); + } + if (schemaCache) { + schemaCache[domain] = restrictions; + } else { + cachedClassRestrictions.set( + schema, + Object.assign(Object.create(null), { + [domain]: restrictions, + }), + ); + } + return restrictions; + } + }; + + const getPredicateRestrictions = (schema, predicate) => { + const schemaCache = cachedPredicateRestrictions.get(schema); + const cached = schemaCache?.[predicate]; + if (cached != null) { + return cached; + } else { + const { objectProperties } = schema; + const restrictions = [ + ...predicateRestrictions(objectProperties, predicate), + ].reduce( + (result, { domainIntersection, rangeIntersection }) => { + result.domainIntersection.push(...domainIntersection); + result.rangeIntersection.push(...rangeIntersection); + return result; + }, + Object.assign(Object.create(null), { + domainIntersection: [], + rangeIntersection: [], + }), + ); + if (schemaCache) { + schemaCache[predicate] = restrictions; + } else { + cachedPredicateRestrictions.set( + schema, + Object.assign(Object.create(null), { + [predicate]: restrictions, + }), + ); + } + return restrictions; + } + }; + + const processSpace = (space) => + Object(space) === space + ? "length" in space + ? Array.from( + space, + (subspace) => + Object(subspace) === subspace + ? Array.from(subspace.unionOf) + : [subspace], + ) + : [Array.from(space.unionOf)] + : [[space]]; + + const predicateRestrictions = function* ( + objectProperties, + predicate, + touched = new Set(), + ) { + if (predicate == "Property" || touched.has(predicate)) { + /* do nothing */ + } else { + const { domain, range, subPropertyOf } = + objectProperties[predicate]; + yield Object.assign(Object.create(null), { + domainIntersection: processSpace(domain ?? "Thing"), + rangeIntersection: processSpace(range ?? "Thing"), + }); + touched.add(predicate); + for ( + const superproperty of ( + subPropertyOf == null + ? ["Property"] + : typeof subPropertyOf == "string" + ? [subPropertyOf] + : Array.from(subPropertyOf) + ) + ) { + yield* predicateRestrictions( + objectProperties, + superproperty, + touched, + ); + } + } + }; + + return { + isObjectPredicateOK: ( + schema, + subjectClass, + predicate, + objectClass, + ) => { + const { objectProperties } = schema; + const predicateDefinition = objectProperties[predicate]; + const isInverse = "inverseOf" in predicateDefinition; + const usedPredicate = isInverse + ? predicateDefinition.inverseOf + : predicate; + const domain = isInverse ? objectClass : subjectClass; + const domains = new Set(getClassAndSuperclasses(schema, domain)); + const ranges = new Set(getClassAndSuperclasses( + schema, + isInverse ? subjectClass : objectClass, + )); + const predicateRestrictions = getPredicateRestrictions( + schema, + usedPredicate, + ); + const { domainIntersection } = predicateRestrictions; + const rangeIntersection = [ + ...predicateRestrictions.rangeIntersection, + ...function* () { + for (const domain of domains) { + const classRestrictionOnPredicate = + getClassRestrictions(schema, domain)[usedPredicate]; + if (classRestrictionOnPredicate != null) { + yield* classRestrictionOnPredicate; + } else { + /* do nothing */ + } + } + }(), + ]; + return domainIntersection.every((domainUnion) => + domainUnion.some((domain) => + domain == "Thing" || domains.has(domain) + ) + ) && + rangeIntersection.every((rangeUnion) => + rangeUnion.some((range) => + range == "Thing" || ranges.has(range) + ) + ); + }, + }; +})(); + +const { + /** + * Returns the provided value converted into a `String` object with + * `.["@value"]` and `.["@language"]` properties. + * + * The same object will be returned for every call with an equivalent + * value. * * TODO: Ideally this would be extracted more fully into an R·D·F * library. @@ -1378,23 +1247,85 @@ const { */ langString, } = (() => { + /** + * Returns the language string object corresponding to the provided + * value and language. + */ + const getLangString = (value, language = "") => { + const valueMap = languageMap[language] ??= Object.create(null); + const literal = valueMap[value]?.deref(); + if (literal != null) { + // There is already an object corresponding to the provided value + // and language. + return literal; + } else { + // No object already exists corresponding to the provided value + // and language; create one. + const result = Object.preventExtensions( + Object.create(String.prototype, { + "@value": { + enumerable: true, + value, + }, + "@language": { + enumerable: !!language, + value: language || null, + }, + language: { enumerable: false, get: getLanguage }, + toString: { enumerable: false, value: toString }, + valueOf: { enumerable: false, value: valueOf }, + }), + ); + const ref = new WeakRef(result); + langStringRegistry.register(result, { ref, language, value }); + valueMap[value] = ref; + return result; + } + }; + /** Returns the `.["@language"]` of this object. */ const getLanguage = Object.defineProperty( function () { - return this["@language"]; + return this["@language"] || null; }, "name", { value: "get language" }, ); + /** + * A `FinalizationRegistry` for language string objects. + * + * This simply cleans up the corresponding `WeakRef` in the language + * map. + */ + const langStringRegistry = new FinalizationRegistry( + ({ ref, language, value }) => { + const valueMap = languageMap[language]; + if (valueMap?.[value] === ref) { + delete valueMap[value]; + } else { + /* do nothing */ + } + }, + ); + + /** + * An object whose own values are an object mapping values to + * language string objects for the language specified by the key. + */ + const languageMap = Object.create(null); + /** Returns the `.["@value"]` of this object. */ const toString = function () { return this["@value"]; }; - /** Returns the `.["@value"]` of this object. */ + /** + * Returns this object if it has a `.["@language"]`; otherwise, its + * `.["@value"]`. + */ const valueOf = function () { - return this["@value"]; + return this["@language"] ? this : this["@value"]; }; return { @@ -1402,37 +1333,15 @@ const { Object($) === $ ? "@value" in $ ? "@language" in $ - ? Object.preventExtensions( - Object.create(String.prototype, { - "@value": { - enumerable: true, - value: `${$["@value"]}`, - }, - "@language": { - enumerable: true, - value: `${$["@language"]}`, - }, - language: { enumerable: false, get: getLanguage }, - toString: { enumerable: false, value: toString }, - valueOf: { enumerable: false, value: valueOf }, - }), + ? getLangString( + `${$["@value"]}`, + `${$["@language"] ?? ""}`, ) - : `${$["@value"]}` + : getLangString(`${$["@value"]}`) : "language" in $ - ? Object.preventExtensions( - Object.create(String.prototype, { - "@value": { enumerable: true, value: `${$}` }, - "@language": { - enumerable: true, - value: `${$.language}`, - }, - language: { enumerable: false, get: getLanguage }, - toString: { enumerable: false, value: toString }, - valueOf: { enumerable: false, value: valueOf }, - }), - ) - : `${$}` - : `${$ ?? ""}`, + ? getLangString(`${$}`, `${$.language ?? ""}`) + : getLangString(`${$}`) + : getLangString(`${$ ?? ""}`), }; })(); @@ -1620,47 +1529,7 @@ export class TagSystem { } else { // No bound constructor has been created yet. const storage = this.#storage; - const BoundTag = Tag.bind(undefined, this, storage); - return this.#Tag = Object.defineProperties(BoundTag, { - all: { - configurable: true, - enumerable: false, - value: Tag.all.bind(BoundTag, this, storage), - writable: true, - }, - fromIRI: { - configurable: true, - enumerable: false, - value: Tag.fromIRI.bind(BoundTag, this, storage), - writable: true, - }, - fromIdentifier: { - configurable: true, - enumerable: false, - value: Tag.fromIdentifier.bind(BoundTag, this, storage), - writable: true, - }, - fromTagURI: { - configurable: true, - enumerable: false, - value: Tag.fromTagURI.bind(BoundTag, this, storage), - writable: true, - }, - identifiers: { - configurable: true, - enumerable: false, - value: Tag.identifiers.bind(BoundTag, this, storage), - writable: true, - }, - name: { value: `${this.tagURI}#${Tag.name}` }, - prototype: { value: Tag.prototype }, - [Storage.toInstance]: { - configurable: true, - enumerable: false, - value: Tag[Storage.toInstance].bind(BoundTag, this, storage), - writable: true, - }, - }); + return this.#Tag = Tag.For(this, storage, schema); } } @@ -1700,7 +1569,14 @@ export class TagSystem { /** Returns the I·R·I for this `TagSystem`. */ get iri() { - return `https://${this.authorityName}/${this.tagURI}`; + return `${this.iriSpace}${this.identifier}`; + } + + /** + * Returns the prefix used for I·R·I’s of `Tag`s in this `TagSystem`. + */ + get iriSpace() { + return `https://${this.authorityName}/tag:${this.taggingEntity}:`; } /** Returns the Tag U·R·I for this `TagSystem`. */