diff --git a/src/TermWrapper.ts b/src/TermWrapper.ts index ac95ff5..192274f 100644 --- a/src/TermWrapper.ts +++ b/src/TermWrapper.ts @@ -1,19 +1,194 @@ -import type { BaseQuad, DataFactory, DatasetCore, Literal, NamedNode, Term } from "@rdfjs/types" -import type { IAnyTerm } from "./type/IAnyTerm.js" +import type { BaseQuad, DataFactory, DatasetCore, Literal, NamedNode, Quad_Subject, Term } from "@rdfjs/types" +import type { IRdfJsTerm } from "./type/IRdfJsTerm.js" -export class TermWrapper implements IAnyTerm { +/** + * `TermWrapper` is one of the two central constructs of this library. It is the base class of all models that represent a mapping from RDF to JavaScript. It _is_ an {@link Term | RDF/JS term} (or node) that also has a reference to both the dataset (or graph) that is the context of (i.e. contains) the term and to a factory that can be used to create additional terms. + * + * @remarks + * This class contains all members of all types derived from {@link Term}. This is so instances of this class can be used _as_ instances of any term type. See relevant example. + * + * @example Basic usage + * The basic pattern of working with this class is to simply extend it and add accessors and mutators (both optional) that expose data from the underlying RDF: + * ```ts + * class SomeClass extends TermWrapper { + * get someProperty(): string { + * return RequiredFrom.subjectPredicate(this, "http://example.com/someProperty", LiteralAs.string) + * } + * + * set someProperty(value: string) { + * RequiredAs.object(this, "http://example.com/someProperty", value, LiteralFrom.string) + * } + * } + * ``` + * + * Assume the following RDF data: + * ```turtle + * BASE + * + * "some value" . + * ``` + * + * We can work with this data in JavaScript and TypeScript as follows: + * ```ts + * const dataset: DatasetCore // which has the RDF above loaded + * const instance = new SomeClass("http://example.com/someSubject", dataset, DataFactory) + * + * const value = instance.someProperty // contains "some value" + * + * instance.someProperty = "some other value" // underlying RDF is now "some other value" . + * ``` + * + * @example Using instances of TermWrapper as instances of RDF/JS Term + * Since this class implements all members of all term types (named nodes, literals, blank nodes etc.), it can be cast to an RDF/JS Term: + * ```ts + * let instance: TermWrapper + * + * // Our instance cast as Term + * const term = instance as Term + * ``` + * + * @example Using instances of TermWrapper to create quads + * Instances of this class can be used anywhere an RDF/JS Term can be used, which includes creating quads: + * ```ts + * let instance: TermWrapper + * let factory: DataFactory + * const predicate = factory.namedNode("http://example.com/p") + * const object = factory.literal("o") + * + * // Our instance used as subject when creating a quad + * factory.quad(instance as Quad_Subject, predicate, object) + * ``` + * + * @example Using instances of TermWrapper to match graph patterns + * Instances of this class can be used anywhere an RDF/JS Term can be used, which includes matching quads in a dataset: + * ```ts + * let instance: TermWrapper + * let dataset: DatasetCore + * + * // Our instance used as subject when matching statements in a dataset + * dataset.match(instance as Term) + * ``` + */ +export class TermWrapper implements IRdfJsTerm { private readonly original: Term + private readonly _dataset: DatasetCore + private readonly _factory: DataFactory - public constructor(term: string, dataset: DatasetCore, factory: DataFactory) - public constructor(term: Term, dataset: DatasetCore, factory: DataFactory) - public constructor(term: string | Term, public readonly dataset: DatasetCore, public readonly factory: DataFactory) { + /** + * Creates a new instance of {@link TermWrapper}. + * + * @param term The IRI of a named node that is the original term being wrapped. + * @param dataset The dataset that contains the term being wrapped. + * @param factory A collection of methods for creating terms. + */ + constructor(term: string, dataset: DatasetCore, factory: DataFactory) + + /** + * Creates a new instance of {@link TermWrapper}. + * + * @param term The original term being wrapped. + * @param dataset The dataset that contains the term being wrapped. + * @param factory A collection of methods for creating terms. + */ + constructor(term: Term, dataset: DatasetCore, factory: DataFactory) + + constructor(term: string | Term, dataset: DatasetCore, factory: DataFactory) { this.original = typeof term === "string" ? factory.namedNode(term) : term + this._dataset = dataset + this._factory = factory } + /** + * The dataset that contains this term. + * + * This accessor provides access to the underlying RDF graph that is the containing context of a node mapped to JavaScript by instances of this class. + * + * @remarks + * RDF/JS, like many other RDF frameworks, keeps terms and datasets separate. This means that terms do not hold a reference to a dataset they reside in (or were found in). This, in turn, means that a dataset must always be available, separate from the term, if either changes to the underlying data or further traversal of the underlying data is called for. In an object-oriented context however, where property chaining is idiomatic (i.e. `instance.property1.property2`), there is no way to supply the dataset when dereferencing a link in the chain. + * + * This property solves the problem by keeping a reference to the dataset. + * + * @exmaple + * Using the dataset to modify information related to this node in the underlying data: + * ```ts + * class Book extends TermWrapper { + * set author(value: string) { + * const subject = this as Quad_Subject + * const predicate = this.factory.namedNode("http://example.com/author") + * const object = this.factory.literal(value) + * const oldAuthors = this.factory.quad(subject, predicate) + * const newAuthor = this.factory.quad(subject, predicate, object) + * + * this.dataset.delete(oldAuthors) + * this.dataset.add(newAuthor) + * } + * } + * ``` + * Note: The above example operates on a low level to explain this property. Library users are more likely to interact with {@link OptionalAs}, {@link RequiredAs} and {@link LiteralFrom} for a better experience. + * + * @exmaple + * Using the dataset to modify data related to this node in the underlying data: + * ```ts + * class Container extends TermWrapper { + * add(something: string) { + * const subject = this as Quad_Subject + * const predicate = this.factory.namedNode("http://example.com/contains") + * const object = this.factory.literal(something) + * const quad = this.factory.quad(subject, predicate, object) + * + * this.dataset.add(quad) + * } + * } + * ``` + */ + get dataset(): DatasetCore { + return this._dataset + } + + /** + * The data factory this instance was instantiated with. A collection of methods that can be used to create terms by this or subsequent wrappers. + * + * @exmaple + * Using the factory to create a literal term from the current date and time: + * ```ts + * class Calendar extends TermWrapper { + * get currentDate(): Literal { + * const date = new Date().toISOString() + * const xsdDateTime = this.factory.namedNode("http://www.w3.org/2001/XMLSchema#dateTime") + * + * return this.factory.literal(date, xsdDateTime) + * } + * } + * ``` + * + * @exmaple + * Using the factory to create a quad: + * ```ts + * class Container extends TermWrapper { + * add(something: string) { + * const subject = this as Quad_Subject + * const predicate = this.factory.namedNode("http://example.com/contains") + * const object = this.factory.literal(something) + * const quad = this.factory.quad(subject, predicate, object) + * + * this.dataset.add(quad) + * } + * } + * ``` + */ + get factory(): DataFactory { + return this._factory + } + + /** + * The well-known property containing a string that represents the type of this object. + */ get [Symbol.toStringTag]() { return this.constructor.name } + //#region Implementation of RDF/JS Term + get termType(): Term["termType"] { return this.original.termType } @@ -22,6 +197,12 @@ export class TermWrapper implements IAnyTerm { return this.original.value } + equals(other: Term | null | undefined): boolean { + return this.original.equals(other) + } + + //#region Implementation of RDF/JS Literal + get language(): string { return (this.original as Literal).language } @@ -34,6 +215,10 @@ export class TermWrapper implements IAnyTerm { return (this.original as Literal).datatype } + //#endregion + + //#region Implementation of RDF/JS Quad + get subject(): Term { return (this.original as BaseQuad).subject } @@ -50,7 +235,7 @@ export class TermWrapper implements IAnyTerm { return (this.original as BaseQuad).graph } - equals(other: Term | null | undefined): boolean { - return this.original.equals(other) - } + //#endregion + + //#endregion } diff --git a/src/ensure.ts b/src/ensure.ts index 31361b4..f2f05f5 100644 --- a/src/ensure.ts +++ b/src/ensure.ts @@ -1,7 +1,7 @@ import type { Literal, Term } from "@rdfjs/types" import { TermTypeError } from "./errors/TermTypeError.js" import { LiteralDatatypeError } from "./errors/LiteralDatatypeError.js" -import type { IAnyTerm } from "./type/IAnyTerm.js" +import type { IRdfJsTerm } from "./type/IRdfJsTerm.js" import { RDF } from "./vocabulary/RDF.js" import { ListRootError } from "./errors/ListRootError.js" @@ -21,7 +21,7 @@ export function ensureIs(object: any, type: Function | { [Symbol.hasInstance](): throw new TypeError(`Object must be a ${type}`) } -export function ensureTermType(term: IAnyTerm, type: Term["termType"]) { +export function ensureTermType(term: IRdfJsTerm, type: Term["termType"]) { if (term.termType === type) { return } @@ -29,7 +29,7 @@ export function ensureTermType(term: IAnyTerm, type: Term["termType"]) { throw new TermTypeError(term as Term, type) } -export function ensureDatatype(term: IAnyTerm, ...datatypes: string[]) { +export function ensureDatatype(term: IRdfJsTerm, ...datatypes: string[]) { if (datatypes.includes(term.datatype.value)) { return } @@ -37,7 +37,7 @@ export function ensureDatatype(term: IAnyTerm, ...datatypes: string[]) { throw new LiteralDatatypeError(term as Literal, datatypes) } -export function ensureListRoot(term: IAnyTerm) { +export function ensureListRoot(term: IRdfJsTerm) { if (term.termType === "NamedNode" && term.value === RDF.nil) { return } diff --git a/src/errors/WrapperError.ts b/src/errors/WrapperError.ts index b11e814..9c25599 100644 --- a/src/errors/WrapperError.ts +++ b/src/errors/WrapperError.ts @@ -13,4 +13,28 @@ export class WrapperError extends Error { this.name = this.constructor.name this.cause = cause } + + //#region Ignore in documentation + + /** @ignore */ + static override captureStackTrace(targetObject: object, constructorOpt?: Function) { + super.captureStackTrace(targetObject, constructorOpt) + } + + /** @ignore */ + static override prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]) { + super.prepareStackTrace(err, stackTraces) + } + + /** @ignore */ + static override get stackTraceLimit() { + return super.stackTraceLimit + } + + /** @ignore */ + static override set stackTraceLimit(value) { + super.stackTraceLimit = value + } + + //#endregion } diff --git a/src/mapping/TermFrom.ts b/src/mapping/TermFrom.ts index 8642cda..c091e15 100644 --- a/src/mapping/TermFrom.ts +++ b/src/mapping/TermFrom.ts @@ -1,5 +1,5 @@ import type { DataFactory, Term } from "@rdfjs/types" -import type { IAnyTerm } from "../type/IAnyTerm.js" +import type { IRdfJsTerm } from "../type/IRdfJsTerm.js" /** * A collection of {@link ITermAsValueMapping | mappers} that create RDF/JS terms from JavaScript primitives. @@ -9,7 +9,7 @@ import type { IAnyTerm } from "../type/IAnyTerm.js" * - [Nodes in RDF 1.1 Concepts and Abstract Syntax](https://www.w3.org/TR/rdf11-concepts/#dfn-node) */ export namespace TermFrom { - export function instance(value: IAnyTerm, factory: DataFactory): Term { + export function instance(value: IRdfJsTerm, factory: DataFactory): Term { return itself(value as Term, factory) } diff --git a/src/mod.ts b/src/mod.ts index f94b515..c46150f 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -2,7 +2,6 @@ export type * from "./type/ITermAsValueMapping.js" export type * from "./type/ITermWrapperConstructor.js" export type * from "./type/ITermFromValueMapping.js" export type * from "./type/ILangString.js" -export type * from "./type/IAnyTerm.js" export * from "./decorators/GetterArity.js" export * from "./decorators/SetterArity.js" diff --git a/src/type/IAnyTerm.ts b/src/type/IAnyTerm.ts deleted file mode 100644 index 8d6f04c..0000000 --- a/src/type/IAnyTerm.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Literal, NamedNode, Term } from "@rdfjs/types" - -export interface IAnyTerm { - readonly termType: Term["termType"] - readonly value: string - readonly language: string - readonly direction: Literal["direction"] - readonly datatype: NamedNode - readonly subject: Term - readonly predicate: Term - readonly object: Term - readonly graph: Term - - equals(other: Term | null | undefined): boolean -} diff --git a/src/type/IRdfJsTerm.ts b/src/type/IRdfJsTerm.ts new file mode 100644 index 0000000..9429103 --- /dev/null +++ b/src/type/IRdfJsTerm.ts @@ -0,0 +1,63 @@ +import type { Literal, NamedNode, Quad, Term } from "@rdfjs/types" + +export interface IRdfJsTerm { + /** + * @see {@link Term.termType} + * @group Implementation of RDF/JS Term + */ + readonly termType: Term["termType"] + + /** + * @see {@link Term.value} + * @group Implementation of RDF/JS Term + */ + readonly value: string + + /** + * @see {@link Literal.language} + * @group Implementation of RDF/JS Term + */ + readonly language: string + + /** + * @see {@link Literal.direction} + * @group Implementation of RDF/JS Term + */ + readonly direction: Literal["direction"] + + /** + * @see {@link Literal.datatype} + * @group Implementation of RDF/JS Term + */ + readonly datatype: NamedNode + + /** + * @see {@link Quad.subject} + * @group Implementation of RDF/JS Term + */ + readonly subject: Term + + /** + * @see {@link Quad.predicate} + * @group Implementation of RDF/JS Term + */ + readonly predicate: Term + + /** + * @see {@link Quad.object} + * @group Implementation of RDF/JS Term + */ + readonly object: Term + + /** + * @see {@link Quad.graph} + * @group Implementation of RDF/JS Term + */ + readonly graph: Term + + /** + * @see {@link Term.equals} + * @group Implementation of RDF/JS Term + */ + equals(other: Term | null | undefined): boolean +} diff --git a/typedoc.json b/typedoc.json index e01bd9c..e5134b7 100644 --- a/typedoc.json +++ b/typedoc.json @@ -22,10 +22,19 @@ "DefaultGraph": "https://rdf.js.org/data-model-spec/#defaultgraph-interface", "Literal": "https://rdf.js.org/data-model-spec/#literal-interface", "Literal.datatype": "https://rdf.js.org/data-model-spec/#dom-literal-datatype", + "Literal.direction": "https://rdf.js.org/data-model-spec/#dom-literal-direction", + "Literal.language": "https://rdf.js.org/data-model-spec/#dom-literal-language", "NamedNode": "https://rdf.js.org/data-model-spec/#namednode-interface", "Quad": "https://rdf.js.org/data-model-spec/#quad-interface", + "Quad.graph": "https://rdf.js.org/data-model-spec/#dom-quad-graph", + "Quad.object": "https://rdf.js.org/data-model-spec/#dom-quad-object", + "Quad.predicate": "https://rdf.js.org/data-model-spec/#dom-quad-predicate", + "Quad.subject": "https://rdf.js.org/data-model-spec/#dom-quad-subject", "Term": "https://rdf.js.org/data-model-spec/#term-interface", - "Variable": "https://rdf.js.org/data-model-spec/#variable-interface" + "termType": "https://rdf.js.org/data-model-spec/#dom-term-termtype", + "Variable": "https://rdf.js.org/data-model-spec/#variable-interface", + "value": "https://rdf.js.org/data-model-spec/#dom-term-value", + "equals": "https://rdf.js.org/data-model-spec/#dom-term-equals" } } }