Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 194 additions & 9 deletions src/TermWrapper.ts
Original file line number Diff line number Diff line change
@@ -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 <http://example.com/>
*
* <someSubject> <someProperty> "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 <someSubject> <someProperty> "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
Comment thread
jeswr marked this conversation as resolved.
* ```
*
* @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)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to https://github.com/rdfjs/wrapper/pull/59/changes#r3069524679 -- does this mean the library actually requires type annotation; and that TermWrapper does not conform to type Term?

If so we should look at implementing TermWrapper so that it implements Term

Suggested change
* dataset.match(instance as Term)
* dataset.match(instance)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love a better mechanism than this.

I did try to make TermWrapper implements Term, but that is impossible vis-à-vis the TypeScript implementation of RDF/JS in @rdfjs/types, which is a union type of interfaces:

export type Term = NamedNode | BlankNode | Literal | Variable | DefaultGraph | BaseQuad;

I'm not saying that anything is incompatible with the RDF/JS spec itself. But neither would I consider moving away from the official typings. It's worth too much in my opinion.

So I would leave this all as is here, and continue on #61, which goes further than I got towards TermWrapper truely being a Term.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I would leave this all as is here, and continue on #61, which goes further than I got towards TermWrapper truely being a Term.

+1

* ```
*/
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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
8 changes: 4 additions & 4 deletions src/ensure.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -21,23 +21,23 @@ 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
}

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
}

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
}
Expand Down
24 changes: 24 additions & 0 deletions src/errors/WrapperError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions src/mapping/TermFrom.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
}

Expand Down
1 change: 0 additions & 1 deletion src/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
15 changes: 0 additions & 15 deletions src/type/IAnyTerm.ts

This file was deleted.

Loading
Loading