Skip to content
Draft
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const dataset_x = new N3.Store(new N3.Parser().parse(rdf)) // rdf holds the turt
Class usage:

```javascript
const person1 = new Person("https://example.org/person1", dataset_x, DataFactory)
const person1 = Person.from("https://example.org/person1", dataset_x, DataFactory)

// Get property
console.log(person1.name)
Expand Down Expand Up @@ -154,7 +154,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)
Expand All @@ -165,7 +165,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)
Expand Down
71 changes: 63 additions & 8 deletions src/DatasetWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { DataFactory, DatasetCore, Quad, Quad_Graph, Term } from "@rdfjs/types"
import type { DataFactory, DatasetCore, Quad, Quad_Graph, Quad_Object, Quad_Subject, Term } from "@rdfjs/types"
import type { ITermWrapperConstructor } from "./type/ITermWrapperConstructor.js"
import type { NamedGraphDataset } from "./NamedGraphDataset.js"
import type { INamedGraphDatasetConstructor } from "./type/INamedGraphDatasetConstructor.js"
import type { TermWrapper } from "./TermWrapper.js"

import { RDF } from "./vocabulary/RDF.js"

Expand Down Expand Up @@ -41,15 +42,45 @@ export class DatasetWrapper implements DatasetCore {

//#region Utilities

protected subjectsOf<T>(predicate: string, termWrapper: ITermWrapperConstructor<T>): Iterable<T> {
/**
* Yields a wrapper around the subject of every statement with the given predicate.
*
* @remarks
* The results are typed as both the wrapper and the {@link Quad.subject | subject term} they wrap, so they can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @param predicate - The IRI of the predicate to match.
* @param termWrapper - A constructor of a class derived from {@link TermWrapper} that wraps each result.
* @returns Wrappers around the subjects of matching statements, typed additionally as the subject terms they wrap.
*/
protected subjectsOf<T extends TermWrapper>(predicate: string, termWrapper: ITermWrapperConstructor<T>): Iterable<T & Quad_Subject> {
return this.matchSubjectsOf(termWrapper, this.factory.namedNode(predicate))
}

protected objectsOf<T>(predicate: string, termWrapper: ITermWrapperConstructor<T>): Iterable<T> {
/**
* Yields a wrapper around the object of every statement with the given predicate.
*
* @remarks
* The results are typed as both the wrapper and the {@link Quad.object | object term} they wrap, so they can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @param predicate - The IRI of the predicate to match.
* @param termWrapper - A constructor of a class derived from {@link TermWrapper} that wraps each result.
* @returns Wrappers around the objects of matching statements, typed additionally as the object terms they wrap.
*/
protected objectsOf<T extends TermWrapper>(predicate: string, termWrapper: ITermWrapperConstructor<T>): Iterable<T & Quad_Object> {
return this.matchObjectsOf(termWrapper, undefined, this.factory.namedNode(predicate))
}

protected instancesOf<T>(klass: string, constructor: ITermWrapperConstructor<T>): Iterable<T> {
/**
* Yields a wrapper around every instance of the given class, that is the subject of every statement with a predicate of `rdf:type` and the given class as the object.
*
* @remarks
* The results are typed as both the wrapper and the {@link Quad.subject | subject term} they wrap, so they can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @param klass - The IRI of the class to match instances of.
* @param constructor - A constructor of a class derived from {@link TermWrapper} that wraps each result.
* @returns Wrappers around the instances, typed additionally as the subject terms they wrap.
*/
protected instancesOf<T extends TermWrapper>(klass: string, constructor: ITermWrapperConstructor<T>): Iterable<T & Quad_Subject> {
return this.matchSubjectsOf(constructor, this.factory.namedNode(RDF.type), this.factory.namedNode(klass))
}

Expand All @@ -69,15 +100,39 @@ export class DatasetWrapper implements DatasetCore {
return new klass(g, this.dataset, this.factory)
}

protected* matchSubjectsOf<T>(termWrapper: ITermWrapperConstructor<T>, predicate?: Term, object?: Term, graph?: Term): Iterable<T> {
/**
* Yields a wrapper around the subject of every statement matching the given pattern.
*
* @remarks
* The results are typed as both the wrapper and the {@link Quad.subject | subject term} they wrap, so they can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @param termWrapper - A constructor of a class derived from {@link TermWrapper} that wraps each result.
* @param predicate - The predicate to match, if any.
* @param object - The object to match, if any.
* @param graph - The graph to match, if any.
* @returns Wrappers around the subjects of matching statements, typed additionally as the subject terms they wrap.
*/
protected* matchSubjectsOf<T extends TermWrapper>(termWrapper: ITermWrapperConstructor<T>, predicate?: Term, object?: Term, graph?: Term): Iterable<T & Quad_Subject> {
for (const q of this.match(undefined, predicate, object, graph)) {
yield new termWrapper(q.subject, this, this.factory)
yield termWrapper.from(q.subject, this, this.factory)
}
}

protected* matchObjectsOf<T>(termWrapper: ITermWrapperConstructor<T>, subject?: Term, predicate?: Term, graph?: Term): Iterable<T> {
/**
* Yields a wrapper around the object of every statement matching the given pattern.
*
* @remarks
* The results are typed as both the wrapper and the {@link Quad.object | object term} they wrap, so they can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @param termWrapper - A constructor of a class derived from {@link TermWrapper} that wraps each result.
* @param subject - The subject to match, if any.
* @param predicate - The predicate to match, if any.
* @param graph - The graph to match, if any.
* @returns Wrappers around the objects of matching statements, typed additionally as the object terms they wrap.
*/
protected* matchObjectsOf<T extends TermWrapper>(termWrapper: ITermWrapperConstructor<T>, subject?: Term, predicate?: Term, graph?: Term): Iterable<T & Quad_Object> {
for (const q of this.match(subject, predicate, undefined, graph)) {
yield new termWrapper(q.object, this, this.factory)
yield termWrapper.from(q.object, this, this.factory)
}
}

Expand Down
105 changes: 105 additions & 0 deletions src/TermWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,111 @@ export class TermWrapper implements IRdfJsTerm {
this._factory = factory
}

//#region Static factory

/**
* Creates a new instance of this class (or subclass) from an IRI, typed as both the wrapper and the {@link NamedNode} it wraps.
*
* @remarks
* This factory is equivalent to invoking the constructor directly, differing only in the return type: the constructor returns the declared instance type, whereas this factory returns the intersection of the (sub)class instance type and {@link NamedNode}. Because the result _is_ a {@link NamedNode} at the type level, it can be passed directly anywhere an RDF/JS {@link Term} is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @example Creating an instance of a subclass
* Assume the following RDF data:
* ```turtle
* BASE <http://example.com/>
*
* <someSubject> <someProperty> "some value" .
* ```
* We can wrap the subject and use the wrapper directly as a named node:
* ```ts
* class SomeClass extends TermWrapper {
* get someProperty(): string {
* return RequiredFrom.subjectPredicate(this, "http://example.com/someProperty", LiteralAs.string)
* }
* }
*
* const instance = SomeClass.from("http://example.com/someSubject", dataset, factory)
* // instance is typed as SomeClass & NamedNode
*
* const value = instance.someProperty // contains "some value"
* ```
*
* @example Using created instances directly as RDF/JS terms
* In contrast to instances created with the constructor, no casts are required to use the result as a term:
* ```ts
* const instance = SomeClass.from("http://example.com/someSubject", dataset, factory)
*
* // Used as subject when creating a quad
* factory.quad(instance, predicate, object)
*
* // Used as subject when matching statements in a dataset
* dataset.match(instance)
* ```
*
* @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.
* @returns A new instance of the class this method was invoked on, typed additionally as the {@link NamedNode} it wraps.
*
* @see
* - [Named nodes in the RDF/JS Data model specification](https://rdf.js.org/data-model-spec/#namednode-interface)
*/
public static from<This extends new (term: string, dataset: DatasetCore, factory: DataFactory) => any>(
this: This,
term: string,
dataset: DatasetCore,
factory: DataFactory,
): InstanceType<This> & NamedNode<string>

/**
* Creates a new instance of this class (or subclass), typed as both the wrapper and the type of the {@link Term} it wraps.
*
* @remarks
* This factory is equivalent to invoking the constructor directly, differing only in the return type: the constructor returns the declared instance type, whereas this factory returns the intersection of the (sub)class instance type and the type of the `term` argument. Because the result _is_ a {@link Term} at the type level, it can be passed directly anywhere an RDF/JS term is expected — for example when creating quads with a {@link DataFactory} or when matching with {@link DatasetCore.match} — without casts.
*
* @example Wrapping a literal
* The term type of the argument is preserved in the return type, so term-type-specific members are available without casts:
* ```ts
* const literal = factory.literal("some value", "en")
* const instance = SomeClass.from(literal, dataset, factory)
* // instance is typed as SomeClass & Literal
*
* const language = instance.language // contains "en"
* ```
*
* @example Using created instances directly as RDF/JS terms
* In contrast to instances created with the constructor, no casts are required to use the result as a term:
* ```ts
* const instance = SomeClass.from(factory.blankNode(), dataset, factory)
*
* // Used as subject when creating a quad
* factory.quad(instance, predicate, object)
*
* // Used as subject when matching statements in a dataset
* dataset.match(instance)
* ```
*
* @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.
* @returns A new instance of the class this method was invoked on, typed additionally as the {@link Term} it wraps.
*
* @see
* - [Terms in the RDF/JS Data model specification](https://rdf.js.org/data-model-spec/#term-interface)
*/
public static from<T extends Term, This extends new (term: T, dataset: DatasetCore, factory: DataFactory) => any>(
this: This,
term: T,
dataset: DatasetCore,
factory: DataFactory,
): InstanceType<This> & T

public static from(this: any, term: string | Term, dataset: DatasetCore, factory: DataFactory): any {
return new this(term, dataset, factory)
}

//#endregion

/**
* The dataset that contains this term.
*
Expand Down
2 changes: 1 addition & 1 deletion src/mapping/TermAs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ensureIs, ensureListRoot, ensurePresent } from "../ensure.js"
* - [Nodes in RDF 1.1 Concepts and Abstract Syntax](https://www.w3.org/TR/rdf11-concepts/#dfn-node)
*/
export namespace TermAs {
export function instance<T>(constructor: ITermWrapperConstructor<T>): ITermAsValueMapping<T> {
export function instance<T extends TermWrapper>(constructor: ITermWrapperConstructor<T>): ITermAsValueMapping<T> {
return (term: TermWrapper) => {
ensurePresent(term)
ensureIs(term, TermWrapper)
Expand Down
14 changes: 10 additions & 4 deletions src/type/ITermWrapperConstructor.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import type { DataFactory, DatasetCore, Term } from "@rdfjs/types"
import type { TermWrapper } from "../TermWrapper.js"

/**
* Represents the constructor signature of term mapping classes that extend {@link TermWrapper}.
* Represents the static side of term mapping classes that extend {@link TermWrapper}: a constructor signature combined with the {@link TermWrapper.from | from static factory}.
*
* Used by this library where constructors of mappers are accepted for implementing navigation properties that represent graph patterns on dataset and term wrappers.
*
* @template T - The type of the mapping class whose instance is created.
* @template T - The type of the mapping class whose instance is created. Must derive from {@link TermWrapper}, which is also the default.
* @param term - The underlying term being wrapped.
* @param dataset - The dataset containing the wrapped term.
* @param factory - The data factory used for creating new terms.
* @returns An instance of the mapping class created when invoking the constructor.
*
* @remarks
* Mapping classes do not need to define a constructor that matches this signature, because the base class has a publicly accessible, matching {@link TermWrapper.constructor | constructor}.
* Mapping classes do not need to define a constructor that matches this signature, because the base class has a publicly accessible, matching {@link TermWrapper.constructor | constructor}. Likewise, they do not need to define the {@link TermWrapper.from | from static factory}, because every class derived from {@link TermWrapper} inherits it automatically.
*
* The library invokes {@link TermWrapper.from} rather than the constructor where it can improve the created instance's type with the type of the term it wraps, for example in {@link DatasetWrapper.matchSubjectsOf} and {@link DatasetWrapper.matchObjectsOf}.
*
* @example Projecting from a dataset to a mapping class
* Given the mapping
Expand Down Expand Up @@ -88,5 +91,8 @@ import type { DataFactory, DatasetCore, Term } from "@rdfjs/types"
* - {@link DatasetWrapper.matchSubjectsOf}
* - {@link DatasetWrapper.objectsOf}
* - {@link TermAs.instance}
* - {@link TermWrapper.from}
*/
export type ITermWrapperConstructor<T> = new (term: Term, dataset: DatasetCore, factory: DataFactory) => T
export type ITermWrapperConstructor<T extends TermWrapper = TermWrapper> =
(new (term: Term, dataset: DatasetCore, factory: DataFactory) => T) &
Pick<typeof TermWrapper, "from">
Loading