diff --git a/library/asynciterator.ts b/library/asynciterator.ts index d2637af..23f7664 100644 --- a/library/asynciterator.ts +++ b/library/asynciterator.ts @@ -1,5 +1,123 @@ +import { AsyncIterator } from "https://esm.sh/asynciterator@3.7.0"; + export { ArrayIterator, type AsyncIterator, MappingIterator, } from "https://esm.sh/asynciterator@3.7.0"; + +type TreeNode = { + [property: string]: T[] | TreeNode; +}; + +export type Tree = T[] | TreeNode; + +type Subtree = { + type: "node"; + tree: TreeNode; + parent?: Subtree; + items: string[]; + path: string[]; + index: number; +} | { + type: "leaf"; + parent?: Subtree; + items: T[]; + path: string[]; + index: number; +}; + +export class TreeIterator extends AsyncIterator<[...string[], T]> { + private _tree: Tree; + private _pointer: Subtree; + + constructor(tree: Tree) { + super(); + this._tree = tree; + this._pointer = this._buildPointerFromSource(tree); + this.readable = true; + } + + read() { + if (this.closed) { + return null; + } + + this._findNextCommonSegment(); + this._findNextLeaf(); + + const p = this._pointer; + if (p.type === "leaf") { + p.index++; + if (p.index < p.items.length) { + return [...p.path, p.items[p.index]] as [...string[], T]; + } + if (!p.parent) { + this.close(); + } + } + if (p.type === "node") { + this.close(); + } + + return null; + } + + protected _buildPointerFromSource( + tree: Tree, + path: string[] = [], + ): Subtree { + if (tree.constructor === Object && !Array.isArray(tree)) { + return { + tree, + parent: this._pointer, + type: "node", + items: Object.keys(tree), + path, + index: -1, + }; + } + if (Array.isArray(tree)) { + return { + parent: this._pointer, + type: "leaf", + items: tree, + path, + index: -1, + }; + } else { + throw new Error( + "Invalid tree specified, expecting arrays in plain objects.", + ); + } + } + + protected _findNextCommonSegment() { + while (this._pointer.parent !== null) { + const p = this._pointer; + if (p.type === "leaf" && p.index < p.items.length - 1) { + // Points to a leaf that is not yet exhausted + break; + } + if (p.index >= p.items.length - 1 && this._pointer.parent) { + this._pointer = this._pointer.parent!; + } else { + break; + } + } + } + + protected _findNextLeaf() { + while (this._pointer.type !== "leaf") { + const p = this._pointer; + p.index++; + if (p.index >= p.items.length) { + // no other keys present, the tree is exhausted; + break; + } + const key = p.items[p.index]; + const source = this._pointer.tree[key]; + this._pointer = this._buildPointerFromSource(source, [...p.path, key]); + } + } +} diff --git a/library/engine/query_engine.ts b/library/engine/query_engine.ts index d84acd6..72e0b01 100644 --- a/library/engine/query_engine.ts +++ b/library/engine/query_engine.ts @@ -6,7 +6,16 @@ import { type RDF, RDFJSON, } from "../rdf.ts"; -import { ArrayIterator, MappingIterator } from "../asynciterator.ts"; +import { + ArrayIterator, + MappingIterator, + TreeIterator, +} from "../asynciterator.ts"; + +type QueryResponseFormat = { + "application/sparql-results+json": RDFJSON.SparqlResultsJsonFormat; + "application/rdf+json": RDFJSON.RdfJsonFormat; +}; export class QueryEngine implements IQueryEngine { protected getSparqlEndpoint(context?: Context) { @@ -47,36 +56,48 @@ export class QueryEngine implements IQueryEngine { return context && context.fetch ? context.fetch : fetch; } - async query(query: string, context?: Context) { + async query< + ResponseType extends keyof QueryResponseFormat, + ResponseFormat = QueryResponseFormat[ResponseType], + >( + query: string, + responseType: ResponseType, + context?: Context, + ) { const endpoint = this.getSparqlEndpoint(context); const fetchFn = this.getFetch(context); - return await fetchFn(endpoint, { + const response = await fetchFn(endpoint, { method: "POST", headers: { - "accept": "application/sparql-results+json", + "accept": responseType, "content-type": "application/x-www-form-urlencoded; charset=UTF-8", }, body: new URLSearchParams({ query, }), }); + const json = await response.json(); + return json as ResponseFormat; } async queryBindings( query: string, context?: Context, ): Promise> { - const result = await this.query(query, context); - const json = await result.json(); + const json = await this.query( + query, + "application/sparql-results+json", + context, + ); - if (!Array.isArray(json?.results?.bindings)) { + if (!Array.isArray(json.results?.bindings)) { throw new Error("Bindings SPARQL query result not found"); } const bindingsFactory = new BindingsFactory(); const bindingsIterator = new ArrayIterator( - json.results.bindings, + json.results!.bindings, ); // TODO: review the unknown type cast @@ -90,8 +111,11 @@ export class QueryEngine implements IQueryEngine { query: string, context?: Context, ): Promise { - const result = await this.query(query, context); - const json = await result.json(); + const json = await this.query( + query, + "application/sparql-results+json", + context, + ); if ("boolean" in json) { return Boolean(json.boolean); } @@ -102,23 +126,20 @@ export class QueryEngine implements IQueryEngine { query: string, context?: Context, ): Promise> { - const result = await this.query(query, context); - const json = await result.json(); + const json = await this.query(query, "application/rdf+json", context); - if (!Array.isArray(json?.results?.bindings)) { + if (!(json?.constructor === Object)) { throw new Error("Quads SPARQL query result not found"); } const quadFactory = new QuadFactory(); - const bindingsIterator = new ArrayIterator( - json.results.bindings, - ); + const treeIterator = new TreeIterator(json); // TODO: review the unknown type cast return new MappingIterator( - bindingsIterator, - (i) => quadFactory.fromJson(i), + treeIterator, + (i) => quadFactory.fromJson(i as [string, string, RDFJSON.Term]), ) as unknown as RDF.ResultStream; } @@ -126,6 +147,6 @@ export class QueryEngine implements IQueryEngine { query: string, context?: Context, ): Promise { - await this.query(query, context); + await this.query(query, "application/sparql-results+json", context); } } diff --git a/library/rdf.ts b/library/rdf.ts index 72d4023..81ab860 100644 --- a/library/rdf.ts +++ b/library/rdf.ts @@ -69,6 +69,16 @@ export declare namespace RDFJSON { datatype?: string; }; type Bindings = Record; + type SparqlResultsJsonFormat = { + head: { + vars?: string[]; + }; + results?: { + bindings: Bindings[]; + }; + boolean?: boolean; + }; + type RdfJsonFormat = Record>; interface TermFactory { fromJson(jsonTerm: Term): RDF.Term; } @@ -76,7 +86,7 @@ export declare namespace RDFJSON { fromJson(jsonBindings: Bindings): RDF.Bindings; } interface QuadFactory { - fromJson(jsonBindings: Bindings): RDF.Quad; + fromJson(jsonRdf: [Iri, Iri, Term]): RDF.Quad; } } @@ -135,21 +145,21 @@ export class BindingsFactory extends ComunicaBindingsFactory export class QuadFactory implements RDFJSON.QuadFactory { protected readonly dataFactory: RDF.DataFactory; - protected readonly bindingsFactory: RDFJSON.BindingsFactory; + protected readonly termFactory: RDFJSON.TermFactory; constructor( dataFactory: RDF.DataFactory = new DataFactory(), - bindingsFactory: RDFJSON.BindingsFactory = new BindingsFactory(dataFactory), + termFactory: RDFJSON.TermFactory = new TermFactory(), ) { this.dataFactory = dataFactory; - this.bindingsFactory = bindingsFactory; + this.termFactory = termFactory; } - fromJson(jsonBindings: RDFJSON.Bindings) { - const bindings = this.bindingsFactory.fromJson(jsonBindings); + fromJson(jsonRdf: [Iri, Iri, RDFJSON.Term]) { + const [s, p, o] = jsonRdf; return this.dataFactory.quad( - bindings.get("s") as RDF.Quad_Subject, - bindings.get("p") as RDF.Quad_Predicate, - bindings.get("o") as RDF.Quad_Object, + this.dataFactory.namedNode(s), + this.dataFactory.namedNode(p), + this.termFactory.fromJson(o) as RDF.Quad_Object, ); } } diff --git a/specs/treeiterator.test.ts b/specs/treeiterator.test.ts new file mode 100644 index 0000000..908e5c3 --- /dev/null +++ b/specs/treeiterator.test.ts @@ -0,0 +1,71 @@ +import { assertEquals } from "./test_deps.ts"; +import { type Tree, TreeIterator } from "../library/asynciterator.ts"; + +async function assertIterator(input: Tree, output: unknown[]) { + const i = new TreeIterator(input); + const result = await i.toArray(); + console.log("RESULT", result); + assertEquals(result, output); +} + +Deno.test("TreeIterator / Empty object", async () => { + const input = {}; + const output: unknown[] = []; + await assertIterator(input, output); +}); + +Deno.test("TreeIterator / Simple array", async () => { + const input = ["hello", "world"]; + const output = [["hello"], ["world"]]; + await assertIterator(input, output); +}); + +Deno.test("TreeIterator / One level tree", async () => { + const input = { + key: ["hello", "world"], + other: ["hello", "world", "!"], + }; + const output = [ + ["key", "hello"], + ["key", "world"], + ["other", "hello"], + ["other", "world"], + ["other", "!"], + ]; + await assertIterator(input, output); +}); + +Deno.test("TreeIterator / Two level tree", async () => { + const input = { + one: { + two: ["hello", "world"], + other: ["!"], + }, + }; + const output = [ + ["one", "two", "hello"], + ["one", "two", "world"], + ["one", "other", "!"], + ]; + await assertIterator(input, output); +}); + +Deno.test("TreeIterator / Asymetric tree", async () => { + const input = { + a: { + aa: ["aaa", "aab"], + ab: { + aba: ["!", "!"], + }, + }, + b: ["bb"], + }; + const output = [ + ["a", "aa", "aaa"], + ["a", "aa", "aab"], + ["a", "ab", "aba", "!"], + ["a", "ab", "aba", "!"], + ["b", "bb"], + ]; + await assertIterator(input, output); +});