diff --git a/README.md b/README.md index 3fe3b79..32e6527 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ ex:person1 ex:name "Alice" . Class usage: ```javascript -const person1 = new Person("https://example.org/person1", dataset_x, DataFactory) +import { DataFactory } from "n3" +const person1 = Person.from("https://example.org/person1", dataset_x, DataFactory) // Get property console.log(person1.name) @@ -149,7 +150,7 @@ ex:person2 Class usage: ```javascript -const person2 = new Person("https://example.org/person2", dataset_z, DataFactory) +const person2 = Person.from("https://example.org/person2", dataset_z, DataFactory) // Get property console.log(person2.name) @@ -160,7 +161,7 @@ console.log(person2.mum.name) // outputs "Alice" // Set class properties -const person3 = new Person("https://example.org/person3", dataset_z, DataFactory) +const person3 = Person.from("https://example.org/person3", dataset_z, DataFactory) person3.name = "Joanne" person1.mum = person3 console.log(person1.mum.name) diff --git a/src/TermWrapper.ts b/src/TermWrapper.ts index 192274f..bad3aef 100644 --- a/src/TermWrapper.ts +++ b/src/TermWrapper.ts @@ -1,5 +1,4 @@ -import type { BaseQuad, DataFactory, DatasetCore, Literal, NamedNode, Quad_Subject, Term } from "@rdfjs/types" -import type { IRdfJsTerm } from "./type/IRdfJsTerm.js" +import type { DataFactory, DatasetCore, NamedNode, Term } from "@rdfjs/types" /** * `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. @@ -31,7 +30,7 @@ import type { IRdfJsTerm } from "./type/IRdfJsTerm.js" * 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 instance = SomeClass.from("http://example.com/someSubject", dataset, DataFactory) * * const value = instance.someProperty // contains "some value" * @@ -39,38 +38,22 @@ import type { IRdfJsTerm } from "./type/IRdfJsTerm.js" * ``` * * @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: + * Instances created via {@link TermWrapper.from} are typed as both the wrapper and the underlying RDF/JS {@link Term}, so they can be passed directly anywhere a `Term` is expected — no casts required: * ```ts - * let instance: TermWrapper + * const instance = SomeClass.from("http://example.com/someSubject", dataset, factory) * - * // 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") + * // Used as subject when creating a quad + * factory.quad(instance, predicate, object) * - * // Our instance used as subject when creating a quad - * factory.quad(instance as Quad_Subject, predicate, object) + * // Used as subject when matching statements in a dataset + * dataset.match(instance) * ``` * - * @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) - * ``` + * @see + * - {@link TermWrapper.from} */ -export class TermWrapper implements IRdfJsTerm { - private readonly original: Term +export class TermWrapper { + private readonly original: T private readonly _dataset: DatasetCore private readonly _factory: DataFactory @@ -93,7 +76,7 @@ export class TermWrapper implements IRdfJsTerm { 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.original = (typeof term === "string" ? factory.namedNode(term) : term) as T this._dataset = dataset this._factory = factory } @@ -189,7 +172,7 @@ export class TermWrapper implements IRdfJsTerm { //#region Implementation of RDF/JS Term - get termType(): Term["termType"] { + get termType(): T["termType"] { return this.original.termType } @@ -201,41 +184,39 @@ export class TermWrapper implements IRdfJsTerm { return this.original.equals(other) } - //#region Implementation of RDF/JS Literal - - get language(): string { - return (this.original as Literal).language - } - - get direction(): Literal["direction"] { - return (this.original as Literal).direction - } - - get datatype(): NamedNode { - return (this.original as Literal).datatype - } - - //#endregion - - //#region Implementation of RDF/JS Quad - - get subject(): Term { - return (this.original as BaseQuad).subject - } - - get predicate(): Term { - return (this.original as BaseQuad).predicate - } - - get object(): Term { - return (this.original as BaseQuad).object - } - - get graph(): Term { - return (this.original as BaseQuad).graph + /** + * Creates a new instance of this class (or subclass), typed as both the wrapper and the underlying RDF/JS {@link Term}. + * + * @remarks + * Equivalent to invoking the constructor directly, but the returned value is typed as the intersection of the (sub)class instance type and the term type. When called on a subclass (e.g. `Child.from(...)`), the returned value is `Child & T`, where `T` is inferred from the `term` argument (defaults to {@link NamedNode} when a string is passed). + * + * @example + * ```ts + * const child = Child.from("http://example.com/x", dataset, factory) + * // typeof child === Child & NamedNode + * ``` + */ + public static from any>( + this: This, + term: string, + dataset: DatasetCore, + factory: DataFactory, + ): InstanceType & NamedNode + public static from any>( + this: This, + term: T, + dataset: DatasetCore, + factory: DataFactory, + ): InstanceType & T + public static from(this: any, term: string | Term, dataset: DatasetCore, factory: DataFactory): any { + return new this(term, dataset, factory) } +} - //#endregion - - //#endregion +for (const prop of ['language', 'direction', 'datatype', 'subject', 'predicate', 'object', 'graph'] as const) { + Object.defineProperty(TermWrapper.prototype, prop, { + get(this: TermWrapper) { return (this as any).original[prop] }, + enumerable: false, + configurable: true, + }) } diff --git a/src/ensure.ts b/src/ensure.ts index 4bef17e..ab507ed 100644 --- a/src/ensure.ts +++ b/src/ensure.ts @@ -1,10 +1,10 @@ import type { DefaultGraph, Literal, Quad, Term } from "@rdfjs/types" import { TermTypeError } from "./errors/TermTypeError.js" import { LiteralDatatypeError } from "./errors/LiteralDatatypeError.js" -import type { IRdfJsTerm } from "./type/IRdfJsTerm.js" import { RDF } from "./vocabulary/RDF.js" import { ListRootError } from "./errors/ListRootError.js" import { NamedGraphError } from "./errors/NamedGraphError.js" +import { TermWrapper } from "./TermWrapper.js" export function ensurePresent(object: any) { if (object !== undefined && object !== null) { @@ -30,15 +30,15 @@ export function ensureTermType(term: { termType: Term["termType"] }, type: Term[ throw new TermTypeError(term as Term, type) } -export function ensureDatatype(term: IRdfJsTerm, ...datatypes: string[]) { - if (datatypes.includes(term.datatype.value)) { +export function ensureDatatype(term: TermWrapper, ...datatypes: string[]) { + if (datatypes.includes((term as unknown as Literal).datatype.value)) { return } - throw new LiteralDatatypeError(term as Literal, datatypes) + throw new LiteralDatatypeError(term as unknown as Literal, datatypes) } -export function ensureListRoot(term: IRdfJsTerm) { +export function ensureListRoot(term: TermWrapper) { if (term.termType === "NamedNode" && term.value === RDF.nil) { return } diff --git a/src/mapping/LiteralAs.ts b/src/mapping/LiteralAs.ts index c09c2dd..c9e9b79 100644 --- a/src/mapping/LiteralAs.ts +++ b/src/mapping/LiteralAs.ts @@ -47,7 +47,7 @@ export namespace LiteralAs { ensureDatatype(term, RDF.langString) // TODO: Direction - return {lang: term.language, string: term.value} + return {lang: (term as unknown as Literal).language, string: term.value} } export function number(term: TermWrapper): number { @@ -152,7 +152,7 @@ export namespace LiteralAs { ensureTermType(term, "Literal") ensureDatatype(term, ...byteArrayDatatypes) - switch (term.datatype.value) { + switch ((term as unknown as Literal).datatype.value) { case XSD.hexBinary: // TODO: When Node 25 - return Uint8Array.fromHex(term.value) return Uint8Array.from(Buffer.from(term.value, "hex")) @@ -179,7 +179,7 @@ export namespace LiteralAs { ensureTermType(term, "Literal") ensureDatatype(term, RDF.langString) - return [term.language, term.value] + return [(term as unknown as Literal).language, term.value] } export function datatypeTuple(term: TermWrapper): [string, string] { @@ -187,7 +187,7 @@ export namespace LiteralAs { ensureIs(term, TermWrapper) ensureTermType(term, "Literal") - return [term.datatype.value, term.value] + return [(term as unknown as Literal).datatype.value, term.value] } } diff --git a/src/mapping/TermFrom.ts b/src/mapping/TermFrom.ts index c091e15..802f50c 100644 --- a/src/mapping/TermFrom.ts +++ b/src/mapping/TermFrom.ts @@ -1,5 +1,5 @@ import type { DataFactory, Term } from "@rdfjs/types" -import type { IRdfJsTerm } from "../type/IRdfJsTerm.js" +import { TermWrapper } from "../TermWrapper.js" /** * A collection of {@link ITermAsValueMapping | mappers} that create RDF/JS terms from JavaScript primitives. @@ -9,7 +9,7 @@ import type { IRdfJsTerm } from "../type/IRdfJsTerm.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: IRdfJsTerm, factory: DataFactory): Term { + export function instance(value: TermWrapper, factory: DataFactory): Term { return itself(value as Term, factory) } diff --git a/src/type/IRdfJsTerm.ts b/src/type/IRdfJsTerm.ts deleted file mode 100644 index 9429103..0000000 --- a/src/type/IRdfJsTerm.ts +++ /dev/null @@ -1,63 +0,0 @@ -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 -}