diff --git a/packages/jsonld-dataset-proxy/src/setProxy/ldSet/BasicLdSet.ts b/packages/jsonld-dataset-proxy/src/setProxy/ldSet/BasicLdSet.ts index c3ef348..9c8066e 100644 --- a/packages/jsonld-dataset-proxy/src/setProxy/ldSet/BasicLdSet.ts +++ b/packages/jsonld-dataset-proxy/src/setProxy/ldSet/BasicLdSet.ts @@ -6,13 +6,11 @@ import { blankNode } from "@rdfjs/data-model"; /* eslint-disable @typescript-eslint/no-explicit-any */ export class BasicLdSet = NonNullable> - extends Set implements LdSet { private hashMap: Map; constructor(values?: Iterable | null) { - super(); this.hashMap = new Map(); if (values) { for (const value of values) { @@ -44,23 +42,17 @@ export class BasicLdSet = NonNullable> const key = this.hashFn(value); if (!this.hashMap.has(key)) { this.hashMap.set(key, value); - super.add(value); } return this; } clear(): void { this.hashMap.clear(); - super.clear(); } delete(value: T): boolean { const key = this.hashFn(value); - if (this.hashMap.has(key)) { - this.hashMap.delete(key); - return super.delete(value); - } - return false; + return this.hashMap.delete(key); } has(value: T): boolean { @@ -68,6 +60,32 @@ export class BasicLdSet = NonNullable> return this.hashMap.has(key); } + get size(): number { + return this.hashMap.size; + } + + *entries(): IterableIterator<[T, T]> { + for (const [, value] of this.hashMap.entries()) { + yield [value, value]; + } + } + + keys(): IterableIterator { + return this.hashMap.values(); + } + + values(): IterableIterator { + return this.hashMap.values(); + } + [Symbol.iterator](): IterableIterator { + return this.hashMap.values(); + } + + get [Symbol.toStringTag]() { + // TODO: Change this to be human readable. + return "BasicLdSet"; + } + /** * =========================================================================== * Array Functions diff --git a/packages/jsonld-dataset-proxy/src/util/RawObject.ts b/packages/jsonld-dataset-proxy/src/util/RawObject.ts index 2d710b4..0ea579c 100644 --- a/packages/jsonld-dataset-proxy/src/util/RawObject.ts +++ b/packages/jsonld-dataset-proxy/src/util/RawObject.ts @@ -1,13 +1,21 @@ import type { BlankNode, NamedNode } from "@rdfjs/types"; import { _getUnderlyingNode } from "../types"; +import type { LdSet } from "../setProxy/ldSet/LdSet"; import type { SubjectProxy } from "../subjectProxy/SubjectProxy"; export type RawObject = | ({ "@id"?: string | NamedNode | BlankNode; } & { - [key: string | symbol | number]: RawValue | RawValue[]; + [key: string | symbol | number]: RawValue | LdSet; }) | SubjectProxy; -export type RawValue = string | boolean | number | RawObject | undefined; +export type RawValue = + | string + | boolean + | number + | RawObject + | NamedNode + | BlankNode + | undefined; diff --git a/packages/jsonld-dataset-proxy/src/util/getNodeFromRaw.ts b/packages/jsonld-dataset-proxy/src/util/getNodeFromRaw.ts index f24942f..58b483b 100644 --- a/packages/jsonld-dataset-proxy/src/util/getNodeFromRaw.ts +++ b/packages/jsonld-dataset-proxy/src/util/getNodeFromRaw.ts @@ -43,7 +43,12 @@ export function getNodeFromRawValue( } else { return literal(value.toString(), datatype); } + } else if ( + typeof value.termType === "string" && + (value.termType === "NamedNode" || value.termType === "BlankNode") + ) { + return value as NamedNode | BlankNode; } else { - return getNodeFromRawObject(value, proxyContext.contextUtil); + return getNodeFromRawObject(value as RawObject, proxyContext.contextUtil); } } diff --git a/packages/jsonld-dataset-proxy/test/BasicLdSet.test.ts b/packages/jsonld-dataset-proxy/test/BasicLdSet.test.ts new file mode 100644 index 0000000..8f9abd9 --- /dev/null +++ b/packages/jsonld-dataset-proxy/test/BasicLdSet.test.ts @@ -0,0 +1,325 @@ +import { namedNode } from "@rdfjs/data-model"; +import jsonldDatasetProxy, { BasicLdSet, _getUnderlyingNode } from "../src"; +import { createDataset } from "@ldo/dataset"; + +describe("BasicLdSet", () => { + describe("constructor and add", () => { + test("should add primitive values correctly", () => { + const set = new BasicLdSet(); + expect(set.size).toBe(0); + set.add(1); + expect(set.size).toBe(1); + expect(set.has(1)).toBe(true); + // Duplicate primitives should not increase size. + set.add(1); + expect(set.size).toBe(1); + }); + + test('should add objects with "@id" as string correctly', () => { + const obj1 = { "@id": "testId" }; + const set = new BasicLdSet(); + set.add(obj1); + expect(set.has(obj1)).toBe(true); + expect(set.size).toBe(1); + // A different object with the same "@id" should be considered a duplicate. + const obj2 = { "@id": "testId" }; + set.add(obj2); + expect(set.size).toBe(1); + }); + + test('should add objects with "@id" as an object correctly', () => { + // In this case the object’s "@id" is a string already. + const obj1 = { "@id": "testIdObj" }; + const set = new BasicLdSet(); + set.add(obj1); + expect(set.has(obj1)).toBe(true); + expect(set.size).toBe(1); + // A different object with an equivalent "@id" should not increase the size. + const obj2 = { "@id": "testIdObj" }; + set.add(obj2); + expect(set.size).toBe(1); + }); + + test("should add LinkedDataObject", () => { + // In this case the object’s "@id" is a string already. + const obj1 = jsonldDatasetProxy(createDataset(), {}).fromSubject( + namedNode("testIdObj"), + ); + const set = new BasicLdSet(); + set.add(obj1); + expect(set.has(obj1)).toBe(true); + expect(set.size).toBe(1); + // A different object with an equivalent "@id" should not increase the size. + const obj2 = { "@id": "testIdObj" }; + set.add(obj2); + expect(set.size).toBe(1); + }); + + test("should add objects with underlying nodes correctly", () => { + // Here we simulate a case where the object has a NamedNode stored as its "@id" + // which in turn yields its .value. + const obj1 = { "@id": namedNode("testIdObj") }; + const set = new BasicLdSet(); + set.add(obj1); + expect(set.has(obj1)).toBe(true); + expect(set.size).toBe(1); + // A different object with an equivalent "@id".value should not increase the size. + const obj2 = { "@id": "testIdObj" }; + set.add(obj2); + expect(set.size).toBe(1); + }); + + test('should treat objects with no "@id" as unique even if same reference', () => { + // When an object does not have "@id" (or _getUnderlyingNode), + // the hashFn falls back to generating a new blank node each time. + const obj = {}; + const set = new BasicLdSet(); + set.add(obj); + // Adding the same object twice produces two different hash keys. + set.add(obj); + expect(set.size).toBe(2); + }); + + test("should initialize with iterable values", () => { + const set = new BasicLdSet([1, 2, 3, 3]); + expect(set.size).toBe(3); + expect([...set]).toEqual([1, 2, 3]); + }); + }); + + describe("clear", () => { + test("should clear all elements", () => { + const set = new BasicLdSet([1, 2, 3]); + expect(set.size).toBe(3); + set.clear(); + expect(set.size).toBe(0); + expect([...set]).toEqual([]); + }); + }); + + describe("delete", () => { + test("should delete an existing element and return true", () => { + const set = new BasicLdSet([1, 2, 3]); + expect(set.delete(2)).toBe(true); + expect(set.has(2)).toBe(false); + expect(set.size).toBe(2); + }); + + test("should return false when deleting a non-existent element", () => { + const set = new BasicLdSet([1, 2, 3]); + expect(set.delete(4)).toBe(false); + expect(set.size).toBe(3); + }); + }); + + describe("has", () => { + test("should correctly identify the presence of elements", () => { + const set = new BasicLdSet([1, 2, 3]); + expect(set.has(1)).toBe(true); + expect(set.has(4)).toBe(false); + }); + }); + + describe("iteration functions", () => { + test("every should return true if all elements satisfy the predicate", () => { + const set = new BasicLdSet([2, 4, 6]); + const result = set.every((num) => num % 2 === 0); + expect(result).toBe(true); + }); + + test("every should return false if any element fails the predicate", () => { + const set = new BasicLdSet([2, 3, 6]); + const result = set.every((num) => num % 2 === 0); + expect(result).toBe(false); + }); + + test("some should return true if any element satisfies the predicate", () => { + const set = new BasicLdSet([1, 3, 4]); + const result = set.some((num) => num % 2 === 0); + expect(result).toBe(true); + }); + + test("some should return false if no element satisfies the predicate", () => { + const set = new BasicLdSet([1, 3, 5]); + const result = set.some((num) => num % 2 === 0); + expect(result).toBe(false); + }); + + test("forEach should call the callback for each element", () => { + const set = new BasicLdSet([1, 2, 3]); + const mockFn = jest.fn(); + set.forEach((value, value2, collection) => { + expect(collection).toBe(set); + expect(value).toBe(value2); + mockFn(value); + }); + expect(mockFn).toHaveBeenCalledTimes(3); + expect(mockFn).toHaveBeenCalledWith(1); + expect(mockFn).toHaveBeenCalledWith(2); + expect(mockFn).toHaveBeenCalledWith(3); + }); + + test("map should return an array with mapped values", () => { + const set = new BasicLdSet([1, 2, 3]); + const result = set.map((num) => num * 2); + expect(result).toEqual([2, 4, 6]); + }); + + test("filter should return a new set with filtered elements", () => { + const set = new BasicLdSet([1, 2, 3, 4]); + const filtered = set.filter((num) => num % 2 === 0); + expect(filtered.size).toBe(2); + expect(filtered.has(2)).toBe(true); + expect(filtered.has(4)).toBe(true); + }); + + test("reduce should work without an initial value", () => { + const set = new BasicLdSet([1, 2, 3, 4]); + const result = set.reduce((acc, curr) => acc + curr); + expect(result).toBe(10); + }); + + test("reduce should work with an initial value", () => { + const set = new BasicLdSet([1, 2, 3, 4]); + const result = set.reduce((acc, curr) => acc + curr, 10); + expect(result).toBe(20); + }); + + test("reduce should throw an error for an empty set without an initial value", () => { + const set = new BasicLdSet(); + expect(() => { + set.reduce((acc, curr) => acc + curr); + }).toThrow("Reduce of empty collection with no initial value"); + }); + + test("toArray and toJSON should return an array of elements", () => { + const elements = [1, 2, 3]; + const set = new BasicLdSet(elements); + expect(set.toArray()).toEqual(elements); + expect(set.toJSON()).toEqual(elements); + }); + }); + + describe("set operations", () => { + test("difference should return elements in the first set not present in the second", () => { + const set1 = new BasicLdSet([1, 2, 3, 4]); + const set2 = new Set([3, 4, 5]); + const diff = set1.difference(set2); + expect(diff.size).toBe(2); + expect(diff.has(1)).toBe(true); + expect(diff.has(2)).toBe(true); + }); + + test("intersection should return only the common elements", () => { + const set1 = new BasicLdSet([1, 2, 3, 4]); + const set2 = new BasicLdSet([3, 4, 5]); + const inter = set1.intersection(set2); + expect(inter.size).toBe(2); + expect(inter.has(3)).toBe(true); + expect(inter.has(4)).toBe(true); + const inter2 = set2.intersection(set1); + expect(inter2.size).toBe(2); + expect(inter2.has(3)).toBe(true); + expect(inter2.has(4)).toBe(true); + }); + + test("isDisjointFrom should return true if the sets have no common elements", () => { + const set1 = new BasicLdSet([1, 2]); + const set2 = new BasicLdSet([3, 4, 5]); + expect(set1.isDisjointFrom(set2)).toBe(true); + expect(set2.isDisjointFrom(set1)).toBe(true); + }); + + test("isDisjointFrom should return false if the sets share elements", () => { + const set1 = new BasicLdSet([1, 2]); + const set2 = new Set([2, 3]); + expect(set1.isDisjointFrom(set2)).toBe(false); + }); + + test("isSubsetOf should return true when the set is a subset of another", () => { + const set1 = new BasicLdSet([1, 2]); + const set2 = new BasicLdSet([1, 2, 3]); + expect(set1.isSubsetOf(set2)).toBe(true); + expect(set2.isSubsetOf(set1)).toBe(false); + }); + + test("isSubsetOf should return false when the set is not a subset of another", () => { + const set1 = new BasicLdSet([1, 2, 4]); + const set2 = new Set([1, 2, 3]); + expect(set1.isSubsetOf(set2)).toBe(false); + }); + + test("isSupersetOf should return true when the set is a superset of another", () => { + const set1 = new BasicLdSet([1, 2, 3]); + const set2 = new Set([1, 2]); + expect(set1.isSupersetOf(set2)).toBe(true); + }); + + test("isSupersetOf should return false when the set is larger", () => { + const set1 = new BasicLdSet([1, 2]); + const set2 = new BasicLdSet([1, 2, 3]); + expect(set1.isSupersetOf(set2)).toBe(false); + }); + + test("isSupersetOf should return false when the set is not a superset of another", () => { + const set1 = new BasicLdSet([1, 2, 5]); + const set2 = new BasicLdSet([1, 2, 3]); + expect(set1.isSupersetOf(set2)).toBe(false); + }); + + test("symmetricDifference should return the symmetric difference of two sets", () => { + const set1 = new BasicLdSet([1, 2, 3]); + const set2 = new Set([2, 3, 4]); + const symDiff = set1.symmetricDifference(set2); + expect(symDiff.size).toBe(2); + expect(symDiff.has(1)).toBe(true); + expect(symDiff.has(4)).toBe(true); + }); + + test("union should return the union of two sets", () => { + const set1 = new BasicLdSet([1, 2]); + const set2 = new Set([2, 3]); + const union = set1.union(set2); + expect(union.size).toBe(3); + expect(union.has(1)).toBe(true); + expect(union.has(2)).toBe(true); + expect(union.has(3)).toBe(true); + }); + }); + + describe("iterator methods", () => { + test("entries returns pairs [value, value]", () => { + const set = new BasicLdSet([1, 2, 3]); + const entries = Array.from(set.entries()); + expect(entries).toEqual([ + [1, 1], + [2, 2], + [3, 3], + ]); + }); + + test("keys returns all values", () => { + const set = new BasicLdSet([1, 2, 3]); + const keys = Array.from(set.keys()); + expect(keys).toEqual([1, 2, 3]); + }); + + test("values returns all values", () => { + const set = new BasicLdSet([1, 2, 3]); + const values = Array.from(set.values()); + expect(values).toEqual([1, 2, 3]); + }); + + test("iterator returns all values", () => { + const set = new BasicLdSet([1, 2, 3]); + const iterated = [...set]; + expect(iterated).toEqual([1, 2, 3]); + }); + + test("toStringTag returns 'BasicLdSet'", () => { + const set = new BasicLdSet(); + expect(Object.prototype.toString.call(set)).toBe("[object BasicLdSet]"); + expect(set[Symbol.toStringTag]).toBe("BasicLdSet"); + }); + }); +});