Skip to content

Commit 88737b7

Browse files
authored
RDF Collections (rdf:List) (#20)
1 parent 3eec175 commit 88737b7

File tree

9 files changed

+724
-3
lines changed

9 files changed

+724
-3
lines changed

src/IndexerInterceptor.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export class IndexerInterceptor<T> implements ProxyHandler<T[]> {
2+
get(target: T[], property: string | symbol, receiver: any): T | undefined {
3+
if (notNumeric(property)) {
4+
return Reflect.get(target, property, receiver)
5+
}
6+
7+
return target.at(Number.parseInt(property))
8+
}
9+
10+
set(target: T[], property: string | symbol, value: T, receiver: any): boolean {
11+
if (notNumeric(property)) {
12+
return Reflect.set(target, property, value, receiver)
13+
}
14+
15+
const i = Number.parseInt(property)
16+
target.fill(value, i, i + 1)
17+
return true
18+
}
19+
20+
deleteProperty(target: T[], property: string | symbol): boolean {
21+
if (notNumeric(property)) {
22+
return Reflect.deleteProperty(target, property)
23+
}
24+
25+
// Elements in this array cannot be deleted because the underlying RDF Collection does not support sparse arrays
26+
return false
27+
}
28+
}
29+
30+
function notNumeric(property: string | symbol): property is symbol {
31+
// TODO: Decide properly
32+
return typeof property === "symbol" || isNaN(parseInt(property))
33+
}

src/ListItem.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { TermWrapper } from "./TermWrapper.js"
2+
import type { DataFactory, DatasetCore, Term } from "@rdfjs/types"
3+
import { ValueMapping } from "./mapping/ValueMapping.js"
4+
import { TermMapping } from "./mapping/TermMapping.js"
5+
import { RDF } from "./vocabulary/RDF.js"
6+
import type { IValueMapping } from "./type/IValueMapping"
7+
import type { ITermMapping } from "./type/ITermMapping.js"
8+
9+
export class ListItem<T> extends TermWrapper {
10+
constructor(term: Term, dataset: DatasetCore, factory: DataFactory, private readonly valueMapping: IValueMapping<T>, private readonly termMapping: ITermMapping<T>) {
11+
super(term, dataset, factory)
12+
}
13+
14+
public get firstRaw(): Term | undefined {
15+
return this.singularNullable(RDF.first, ValueMapping.asIs)
16+
}
17+
18+
public set firstRaw(value: Term | undefined) {
19+
this.overwriteNullable(RDF.first, value, TermMapping.asIs)
20+
}
21+
22+
public get restRaw(): Term | undefined {
23+
return this.singularNullable(RDF.rest, ValueMapping.asIs)
24+
}
25+
26+
public set restRaw(value: Term | undefined) {
27+
this.overwriteNullable(RDF.rest, value, TermMapping.asIs)
28+
}
29+
30+
public get isListItem(): boolean {
31+
return this.firstRaw !== undefined && this.restRaw !== undefined
32+
}
33+
34+
public get isNil(): boolean {
35+
return this.term.equals(this.factory.namedNode(RDF.nil))
36+
}
37+
38+
public get first(): T {
39+
return this.singular(RDF.first, this.valueMapping)
40+
}
41+
42+
public set first(value: T) {
43+
this.overwrite(RDF.first, value, this.termMapping)
44+
}
45+
46+
public get rest(): ListItem<T> {
47+
return this.singular(RDF.rest, w => new ListItem(w.term, w.dataset, w.factory, this.valueMapping, this.termMapping))
48+
}
49+
50+
public set rest(value: ListItem<T>) {
51+
this.overwrite(RDF.rest, value, TermMapping.identity)
52+
}
53+
54+
public pop(): T {
55+
try {
56+
return this.first
57+
} finally {
58+
this.firstRaw = undefined
59+
this.restRaw = this.factory.namedNode(RDF.nil)
60+
}
61+
}
62+
63+
public* items(): Iterable<ListItem<T>> {
64+
if (this.firstRaw === undefined) {
65+
return
66+
}
67+
68+
yield this
69+
70+
for (const more of this.rest.items()) {
71+
yield more
72+
}
73+
}
74+
}

src/Overwriter.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { TermWrapper } from "./TermWrapper.js"
2+
import { ListItem } from "./ListItem.js"
3+
import { TermMapping } from "./mapping/TermMapping.js"
4+
5+
export class Overwriter<T> extends TermWrapper {
6+
constructor(subject: TermWrapper, private readonly predicate: string) {
7+
super(subject.term, subject.dataset, subject.factory);
8+
}
9+
10+
set listNode(object: ListItem<T>) {
11+
this.overwrite(this.predicate, object, TermMapping.identity)
12+
}
13+
}

src/RdfList.ts

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { TermWrapper } from "./TermWrapper.js"
2+
import type { IValueMapping } from "./type/IValueMapping.js"
3+
import type { ITermMapping } from "./type/ITermMapping.js"
4+
import { IndexerInterceptor } from "./IndexerInterceptor.js"
5+
import { ListItem } from "./ListItem.js"
6+
import { RDF } from "./vocabulary/RDF.js"
7+
import type { Term } from "@rdfjs/types"
8+
import { Overwriter } from "./Overwriter.js"
9+
10+
export class RdfList<T> implements Array<T> {
11+
private root: ListItem<T>
12+
13+
constructor(root: Term, private readonly subject: TermWrapper, private readonly predicate: string, private readonly valueMapping: IValueMapping<T>, private readonly termMapping: ITermMapping<T>) {
14+
this.root = new ListItem(root, this.subject.dataset, this.subject.factory, valueMapping, termMapping)
15+
16+
// TODO: Singleton interceptor?
17+
return new Proxy(this, new IndexerInterceptor<T>)
18+
}
19+
20+
// Never invoked, intercepted by proxy
21+
[n: number]: T
22+
23+
get [Symbol.unscopables](): { [K in keyof any[]]?: boolean } {
24+
return Array.prototype[Symbol.unscopables]
25+
}
26+
27+
get length(): number {
28+
return [...this.items].length
29+
}
30+
31+
set length(_: number) {
32+
throw new Error("this array is based on an RDF Collection. Its length cannot be modified like this.")
33+
}
34+
35+
[Symbol.iterator](): ArrayIterator<T> {
36+
return this.values()
37+
}
38+
39+
at(index: number): T | undefined {
40+
// TODO: Don't materialize all, only up to index
41+
return [...this.items].at(index)?.first
42+
}
43+
44+
concat(...items: Array<ConcatArray<T> | T>): T[] {
45+
return [...this].concat(...items)
46+
}
47+
48+
copyWithin(target: number, start: number, end?: number): this {
49+
throw new Error("not implemented")
50+
}
51+
52+
entries(): ArrayIterator<[number, T]> {
53+
// TODO: Don't materlialize all upfront
54+
return [...this].entries()
55+
}
56+
57+
every<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): this is S[] {
58+
return [...this].every(predicate, thisArg)
59+
}
60+
61+
fill(value: T, start?: number, end?: number): this {
62+
throw new Error("not implemented")
63+
}
64+
65+
filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[] {
66+
return [...this].filter(predicate, thisArg)
67+
}
68+
69+
find<S extends T>(predicate: (value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined {
70+
return [...this].find(predicate, thisArg)
71+
}
72+
73+
findIndex(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): number {
74+
return [...this].findIndex(predicate, thisArg)
75+
}
76+
77+
flat<A, D extends number = 1>(depth?: D): FlatArray<A, D>[] {
78+
throw new Error("not implemented")
79+
}
80+
81+
flatMap<U, This = undefined>(callback: (this: This, value: T, index: number, array: T[]) => (ReadonlyArray<U> | U), thisArg?: This): U[] {
82+
return [...this].flatMap(callback, thisArg)
83+
}
84+
85+
forEach(callback: (value: T, index: number, array: T[]) => void, thisArg?: any): void {
86+
[...this].forEach(callback, thisArg)
87+
}
88+
89+
includes(searchElement: T, fromIndex?: number): boolean {
90+
return [...this].includes(searchElement, fromIndex)
91+
}
92+
93+
indexOf(searchElement: T, fromIndex?: number): number {
94+
return [...this].indexOf(searchElement, fromIndex)
95+
}
96+
97+
join(separator?: string): string {
98+
return [...this].join(separator)
99+
}
100+
101+
keys(): ArrayIterator<number> {
102+
// TODO: Don't materialize all upfront
103+
return [...this.items].keys()
104+
}
105+
106+
lastIndexOf(searchElement: T, fromIndex?: number): number {
107+
return [...this].lastIndexOf(searchElement, fromIndex)
108+
}
109+
110+
map<U>(callback: (value: T, index: number, array: T[]) => U, thisArg?: any): U[] {
111+
return [...this].map(callback, thisArg)
112+
}
113+
114+
pop(): T | undefined {
115+
return [...this.items].at(-1)?.pop()
116+
}
117+
118+
push(...items: T[]): number {
119+
const nil = this.subject.factory.namedNode(RDF.nil)
120+
121+
for (const item of items) {
122+
// A node will be needed either to replace rdf:nil in an empty list or to add a new one to the end of an existing list
123+
const newNode = new ListItem(this.subject.factory.blankNode(), this.subject.dataset, this.subject.factory, this.valueMapping, this.termMapping)
124+
125+
const lastNode = this.root.isNil ?
126+
// The statement representing an empty list is replaced by a new one whose object is the new node
127+
// The representation of the first item (root, currently rdf:nil, the empty list) is overwritten by the new node
128+
this.root = new Overwriter<T>(this.subject, this.predicate).listNode = newNode :
129+
130+
// replace rest of current last with new and return is because it's the new last
131+
[...this.items].at(-1)!.rest = newNode;
132+
133+
lastNode.first = item
134+
lastNode.restRaw = nil
135+
}
136+
137+
return this.length
138+
}
139+
140+
reduce<U>(callback: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue?: U): U {
141+
return [...this].reduce(callback, initialValue!)
142+
}
143+
144+
reduceRight<U>(callback: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue?: U): U {
145+
return [...this].reduceRight(callback, initialValue!)
146+
}
147+
148+
reverse(): T[] {
149+
throw new Error("not implemented")
150+
}
151+
152+
shift(): T | undefined {
153+
if (this.root.isNil) {
154+
return undefined
155+
}
156+
157+
const value = this.root.first
158+
159+
if (this.root.rest.isNil) {
160+
new Overwriter<T>(this.subject, this.predicate).listNode = this.root.rest
161+
this.root.firstRaw = undefined
162+
this.root.restRaw = undefined
163+
} else {
164+
this.root.firstRaw = this.root.rest.firstRaw
165+
this.root.restRaw = this.root.rest.restRaw
166+
}
167+
168+
return value
169+
}
170+
171+
slice(start?: number, end?: number): T[] {
172+
// TODO: Probably no need to materialize all
173+
return [...this].slice(start, end)
174+
}
175+
176+
some(predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any): boolean {
177+
return [...this].some(predicate, thisArg)
178+
}
179+
180+
sort(compareFn?: (a: T, b: T) => number): this {
181+
throw new Error("not implemented")
182+
}
183+
184+
splice(start: number, deleteCount?: number, ...items: T[]): T[] {
185+
throw new Error("not implemented")
186+
}
187+
188+
unshift(...items: T[]): number {
189+
for (const item of items.reverse()) {
190+
const firstNode = this.root
191+
this.root = new Overwriter<T>(this.subject, this.predicate).listNode = new ListItem(this.subject.factory.blankNode(), this.subject.dataset, this.subject.factory, this.valueMapping, this.termMapping)
192+
this.root.first = item
193+
this.root.rest = firstNode
194+
}
195+
196+
return this.length
197+
}
198+
199+
* values(): ArrayIterator<T> {
200+
for (const item of this.items) {
201+
yield item.first
202+
}
203+
}
204+
205+
private get items(): Iterable<ListItem<T>> {
206+
return this.root.items()
207+
}
208+
}

src/mapping/ObjectMapping.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { IValueMapping } from "../type/IValueMapping.js"
22
import type { ITermWrapperConstructor } from "../type/ITermWrapperConstructor.js"
33
import type { TermWrapper } from "../TermWrapper.js"
4+
import type { ITermMapping } from "../type/ITermMapping.js"
5+
import { RdfList } from "../RdfList.js"
46

57

68
/*
@@ -18,4 +20,8 @@ export namespace ObjectMapping {
1820
export function as<T>(constructor: ITermWrapperConstructor<T>): IValueMapping<T> {
1921
return (termWrapper: TermWrapper) => new constructor(termWrapper.term, termWrapper.dataset, termWrapper.factory)
2022
}
23+
24+
export function asList<T>(subject: TermWrapper, predicate: string, valueMapping: IValueMapping<T>, termMapping: ITermMapping<T>): IValueMapping<T[]> {
25+
return w => new RdfList(w.term, subject, predicate, valueMapping, termMapping)
26+
}
2127
}

src/mapping/TermMapping.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DataFactory, DatasetCore } from "@rdfjs/types"
1+
import type { DataFactory, DatasetCore, Term } from "@rdfjs/types"
22
import type { ILangString } from "../type/ILangString.js"
33

44
import { TermWrapper } from "../TermWrapper.js"
@@ -37,4 +37,12 @@ export namespace TermMapping {
3737
export function stringToLiteral(value: string, dataset: DatasetCore, factory: DataFactory): TermWrapper | undefined {
3838
return new TermWrapper(factory.literal(value), dataset, factory)
3939
}
40+
41+
export function asIs(value: Term, dataset: DatasetCore, factory: DataFactory): TermWrapper | undefined {
42+
return new TermWrapper(value, dataset, factory)
43+
}
44+
45+
export function identity<T>(value: T): T {
46+
return value
47+
}
4048
}

src/mapping/ValueMapping.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Literal } from "@rdfjs/types"
1+
import type { Literal, Term } from "@rdfjs/types"
22
import type { ILangString } from "../type/ILangString.js"
33
import type { TermWrapper } from "../TermWrapper.js"
44

@@ -46,4 +46,8 @@ export namespace ValueMapping {
4646
export function iriOrBlankNodeToString(termWrapper: TermWrapper): string {
4747
return termWrapper.term.value
4848
}
49+
50+
export function asIs(termWrapper: TermWrapper): Term {
51+
return termWrapper.term
52+
}
4953
}

src/vocabulary/RDF.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export const RDF = {
22
langString: "http://www.w3.org/1999/02/22-rdf-syntax-ns#langString",
3-
type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
3+
type: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
4+
first: "http://www.w3.org/1999/02/22-rdf-syntax-ns#first",
5+
rest: "http://www.w3.org/1999/02/22-rdf-syntax-ns#rest",
6+
nil: "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil",
47
} as const

0 commit comments

Comments
 (0)