Skip to content

Commit

Permalink
Implemented tree iterator for quads query
Browse files Browse the repository at this point in the history
  • Loading branch information
karelklima committed Sep 19, 2022
1 parent 673bb5d commit 0b9fe96
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 28 deletions.
118 changes: 118 additions & 0 deletions library/asynciterator.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,123 @@
import { AsyncIterator } from "https://esm.sh/[email protected]";

export {
ArrayIterator,
type AsyncIterator,
MappingIterator,
} from "https://esm.sh/[email protected]";

type TreeNode<T> = {
[property: string]: T[] | TreeNode<T>;
};

export type Tree<T> = T[] | TreeNode<T>;

type Subtree<T> = {
type: "node";
tree: TreeNode<T>;
parent?: Subtree<T>;
items: string[];
path: string[];
index: number;
} | {
type: "leaf";
parent?: Subtree<T>;
items: T[];
path: string[];
index: number;
};

export class TreeIterator<T> extends AsyncIterator<[...string[], T]> {
private _tree: Tree<T>;
private _pointer: Subtree<T>;

constructor(tree: Tree<T>) {
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<T>,
path: string[] = [],
): Subtree<T> {
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]);
}
}
}
59 changes: 40 additions & 19 deletions library/engine/query_engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<RDF.ResultStream<RDF.Bindings>> {
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<RDFJSON.Bindings>(
json.results.bindings,
json.results!.bindings,
);

// TODO: review the unknown type cast
Expand All @@ -90,8 +111,11 @@ export class QueryEngine implements IQueryEngine {
query: string,
context?: Context,
): Promise<boolean> {
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);
}
Expand All @@ -102,30 +126,27 @@ export class QueryEngine implements IQueryEngine {
query: string,
context?: Context,
): Promise<RDF.ResultStream<RDF.Quad>> {
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<RDFJSON.Bindings>(
json.results.bindings,
);
const treeIterator = new TreeIterator<RDFJSON.Term>(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<RDF.Quad>;
}

async queryVoid(
query: string,
context?: Context,
): Promise<void> {
await this.query(query, context);
await this.query(query, "application/sparql-results+json", context);
}
}
28 changes: 19 additions & 9 deletions library/rdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,24 @@ export declare namespace RDFJSON {
datatype?: string;
};
type Bindings = Record<string, Term>;
type SparqlResultsJsonFormat = {
head: {
vars?: string[];
};
results?: {
bindings: Bindings[];
};
boolean?: boolean;
};
type RdfJsonFormat = Record<Iri, Record<Iri, Term[]>>;
interface TermFactory {
fromJson(jsonTerm: Term): RDF.Term;
}
interface BindingsFactory {
fromJson(jsonBindings: Bindings): RDF.Bindings;
}
interface QuadFactory {
fromJson(jsonBindings: Bindings): RDF.Quad;
fromJson(jsonRdf: [Iri, Iri, Term]): RDF.Quad;
}
}

Expand Down Expand Up @@ -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,
);
}
}
71 changes: 71 additions & 0 deletions specs/treeiterator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { assertEquals } from "./test_deps.ts";
import { type Tree, TreeIterator } from "../library/asynciterator.ts";

async function assertIterator(input: Tree<unknown>, 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);
});

0 comments on commit 0b9fe96

Please sign in to comment.