From 05a351899bbf4970c8530b81f556e998703a0639 Mon Sep 17 00:00:00 2001 From: Jesse Wright <63333554+jeswr@users.noreply.github.com> Date: Mon, 6 Jul 2026 01:09:58 +0000 Subject: [PATCH] feat: export WrappingSet and narrow SetFrom's return type to WrappingSet WrappingSet is the concrete type behind every set-mapped model property, but it was internal, so its type could not be named by consuming code and SetFrom.subjectPredicate could only advertise the plain Set interface. - Export WrappingSet from mod.ts - Narrow SetFrom.subjectPredicate's return type from Set to WrappingSet (non-breaking: WrappingSet implements Set) - Document the WrappingSet class and constructor - Test that set-mapped model properties are WrappingSet instances Co-Authored-By: Claude Fable 5 --- src/WrappingSet.ts | 59 +++++++++++++++++++++++++++++++++- src/mapping/SetFrom.ts | 2 +- src/mod.ts | 1 + test/unit/wrapping_set.test.ts | 21 ++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 test/unit/wrapping_set.test.ts diff --git a/src/WrappingSet.ts b/src/WrappingSet.ts index 6f163ff..9ec13d0 100644 --- a/src/WrappingSet.ts +++ b/src/WrappingSet.ts @@ -1,10 +1,67 @@ import type { ITermAsValueMapping } from "./type/ITermAsValueMapping.js" import type { ITermFromValueMapping } from "./type/ITermFromValueMapping.js" -import type { DatasetCore, Quad, Quad_Object, Quad_Subject, Term } from "@rdfjs/types" +import type { DatasetCore, NamedNode, Quad, Quad_Object, Quad_Subject, Term } from "@rdfjs/types" import { TermWrapper } from "./TermWrapper.js" +/** + * A {@link Set} view over the objects of all ` ?o` + * quads in the underlying dataset. + * + * @remarks + * The set is **live**: iteration, {@link size} and {@link has} re-query the + * dataset on every call, so the contents always reflect the current state. + * Mutations performed via {@link add}, {@link delete} and {@link clear} + * write through to the underlying dataset. + * + * Application code typically obtains instances through + * {@link SetFrom.subjectPredicate}, which constructs a fresh + * {@link WrappingSet} on every property access. + * + * @example Exposing a set-valued property on a model + * Assume the following RDF data: + * ```turtle + * BASE + * + * "some value", "some other value" . + * ``` + * + * A model can expose the objects of `someProperty` as a mutable set of strings: + * ```ts + * class SomeClass extends TermWrapper { + * get someProperty(): WrappingSet { + * return SetFrom.subjectPredicate(this, "http://example.com/someProperty", LiteralAs.string, LiteralFrom.string) + * } + * } + * + * const instance = new SomeClass("http://example.com/someSubject", dataset, DataFactory) + * + * instance.someProperty.size // 2 + * instance.someProperty.add("a third value") // the underlying dataset now contains a third statement + * ``` + * + * @see + * - {@link SetFrom.subjectPredicate} + */ export class WrappingSet implements Set { // TODO: Direction + + /** + * Constructs a {@link WrappingSet}. + * + * Application code typically does not call this constructor directly; + * use {@link SetFrom.subjectPredicate} instead, which produces a + * {@link WrappingSet} for a given anchor / predicate / mapping triple. + * + * @param subject The anchor {@link TermWrapper} - all quads in this + * set have this term as their subject. + * @param predicate The IRI of the predicate - all quads in this set + * have a {@link NamedNode} with this IRI as their + * predicate. + * @param termAs Mapping from RDF object to JavaScript value, used by + * iteration. + * @param termFrom Mapping from JavaScript value to RDF object, used by + * {@link add}, {@link delete} and {@link has}. + */ public constructor(private readonly subject: TermWrapper, private readonly predicate: string, private readonly termAs: ITermAsValueMapping, private readonly termFrom: ITermFromValueMapping) { } diff --git a/src/mapping/SetFrom.ts b/src/mapping/SetFrom.ts index 9ba0e08..bbbab78 100644 --- a/src/mapping/SetFrom.ts +++ b/src/mapping/SetFrom.ts @@ -7,7 +7,7 @@ import { WrappingSet } from "../WrappingSet.js" * A collection of {@link ITermFromValueMapping | mappers} that expose RDF/JS graph patterns as mutable JavaScript {@link Set | sets}. */ export namespace SetFrom { - export function subjectPredicate(anchor: TermWrapper, p: string, termAs: ITermAsValueMapping, termFrom: ITermFromValueMapping): Set { + export function subjectPredicate(anchor: TermWrapper, p: string, termAs: ITermAsValueMapping, termFrom: ITermFromValueMapping): WrappingSet { if (termAs === undefined) { throw new Error // TODO: Describe } diff --git a/src/mod.ts b/src/mod.ts index 016c51d..1ea753a 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -20,6 +20,7 @@ export * from "./mapping/RequiredAs.js" export * from "./DatasetWrapper.js" export * from "./TermWrapper.js" +export * from "./WrappingSet.js" export * from "./NamedGraphDataset.js" export * from "./errors/WrapperError.js" diff --git a/test/unit/wrapping_set.test.ts b/test/unit/wrapping_set.test.ts new file mode 100644 index 0000000..5164f7b --- /dev/null +++ b/test/unit/wrapping_set.test.ts @@ -0,0 +1,21 @@ +import assert from "node:assert" +import { describe, it } from "node:test" +import { LiteralAs, LiteralFrom, SetFrom, TermWrapper, WrappingSet } from "@rdfjs/wrapper" +import { DataFactory } from "n3" +import { datasetFromRdf } from "./util/datasetFromRdf.js" + +class Wrapper extends TermWrapper { + public get strings(): WrappingSet { + return SetFrom.subjectPredicate(this, "p", LiteralAs.string, LiteralFrom.string) + } +} + +await describe("Wrapping set", async () => { + await it("is returned by set-mapped model properties", async () => { + const rdf = `

"o1", "o2" .` + const wrapper = new Wrapper("s", datasetFromRdf(rdf), DataFactory) + + assert.strictEqual(wrapper.strings instanceof WrappingSet, true) + assert.strictEqual(wrapper.strings.size, 2) + }) +})