Skip to content

Commit

Permalink
Support static values for predicates
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimvh authored and RubenVerborgh committed Apr 24, 2020
1 parent 4a8631e commit 11ccd86
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 9 deletions.
18 changes: 17 additions & 1 deletion src/JSONLDResolver.js
Expand Up @@ -31,7 +31,23 @@ export default class JSONLDResolver {
const reverse = lazyThenable(() => this._context.then(context =>
context[property] && context[property]['@reverse']));
const resultsCache = this.getResultsCache(pathData, predicate, reverse);
return pathData.extendPath({ property, predicate, resultsCache, reverse });
return pathData.extendPath({ property, predicate, resultsCache, reverse, asFunction: this.apply });
}

/**
* Function for fixing the object values of the predicate.
* Extends the proxy with the new values.
*/
apply(pathData, args) {
if (!args || args.length === 0)
throw new Error('At least 1 argument is required');
if (args.some(arg => !arg.termType))
throw new Error('Args need to be RDF objects');
pathData.values = args;
// Prevent children from inheriting this apply function
delete pathData.asFunction;
// No extendPath call since we don't want to add a chain so return original proxy
return pathData.proxy;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/PathExpressionHandler.js
Expand Up @@ -14,6 +14,7 @@ export default class PathExpressionHandler {
predicate: await current.predicate,
reverse: await current.reverse,
sort: current.sort,
values: current.values,
});
}
// Move to parent link
Expand Down
17 changes: 15 additions & 2 deletions src/PathProxy.js
Expand Up @@ -35,7 +35,10 @@ export default class PathProxy {
[data, settings] = [settings, {}];

// Create the path's internal data object and the proxy that wraps it
const path = { settings, ...data };
// This needs to be a function or `apply` can't be proxied
// eslint-disable-next-line no-empty-function
function path() { }
Object.assign(path, { settings, ...data });
const proxy = path.proxy = new Proxy(path, this);

// Add an extendPath method to create child paths
Expand Down Expand Up @@ -66,8 +69,18 @@ export default class PathProxy {
if (resolver.supports(property))
return resolver.resolve(property, pathData, pathData.proxy);
}

// Otherwise, the property does not exist
return undefined;
}

/**
* Handles calling the proxy as a function
*/
apply(pathData, thisArg, args) {
if (pathData.asFunction && typeof pathData.asFunction === 'function')
return pathData.asFunction(pathData, args);

// Otherwise, there is no apply handler
return undefined;
}
}
26 changes: 21 additions & 5 deletions src/SparqlHandler.js
Expand Up @@ -100,12 +100,29 @@ export default class SparqlHandler {
let object = this.termToString(skolemize(root.subject));
let queryVar = object;
const sorts = [];
const clauses = pathExpression.map((segment, index) => {
const clauses = [];
let staticValid = false;
pathExpression.forEach((segment, index) => {
// Obtain components and generate triple pattern
const subject = object;
const { predicate, reverse, sort } = segment;
object = index < lastIndex ? this.createVar(`v${index}`, scope) : lastVar;
const patttern = this.triplePattern(subject, predicate, object, reverse);
const { predicate, reverse, sort, values } = segment;
// Don't update variable if there are static values instead
if (values && values.length > 0) {
if (!staticValid)
throw new Error('Can not have static objects if the subject is also static');
// Can't have 2 subsequent static triple sets
staticValid = false;
const valueStrs = values.map(v => this.termToString(v));
if (reverse)
clauses.push(...valueStrs.map(v => this.triplePattern(v, predicate, subject)));
else
clauses.push(this.triplePattern(subject, predicate, valueStrs.join(', ')));
}
else {
staticValid = true;
object = index < lastIndex ? this.createVar(`v${index}`, scope) : lastVar;
clauses.push(this.triplePattern(subject, predicate, object, reverse));
}

// If the sort option was not set, use this object as a query variable
if (!sort) {
Expand All @@ -118,7 +135,6 @@ export default class SparqlHandler {
// TODO: use a descriptive lastVar in case of sorting
object = queryVar;
}
return patttern;
});
return { queryVar, sorts, clauses };
}
Expand Down
13 changes: 13 additions & 0 deletions test/integration/execute-query-test.js
Expand Up @@ -9,11 +9,13 @@ import SubjectsHandler from '../../src/SubjectsHandler';
import SetFunctionHandler from '../../src/SetFunctionHandler';
import DataHandler from '../../src/DataHandler';
import JSONLDResolver from '../../src/JSONLDResolver';
import MutationExpressionsHandler from '../../src/MutationExpressionsHandler';
import { createQueryEngine, deindent } from '../util';
import { namedNode, literal } from '@rdfjs/data-model';
import { iterableToArray } from '../../src/iterableUtils';

import context from '../context';
import ThenHandler from '../../src/ThenHandler';

const subject = namedNode('https://example.org/#me');
const queryEngine = createQueryEngine([
Expand All @@ -26,6 +28,8 @@ const resolvers = [
new JSONLDResolver(context),
];
const handlersPath = {
then: new ThenHandler(),
mutationExpressions: new MutationExpressionsHandler(),
sparql: new SparqlHandler(),
pathExpression: new PathExpressionHandler(),
results: new ExecuteQueryHandler(),
Expand All @@ -38,6 +42,8 @@ const handlersPath = {
};

const handlersMutation = {
then: new ThenHandler(),
mutationExpressions: new MutationExpressionsHandler(),
sparql: new SparqlHandler(),
pathExpression: new PathExpressionHandler(),
results: new ExecuteQueryHandler(),
Expand Down Expand Up @@ -122,6 +128,13 @@ describe('a query path with a path and mutation expression handler', () => {
expect(names.map(n => `${n}`)).toEqual(['Alice', 'Bob', 'Carol']);
});

it('returns results for a path with 4 links and fixed values', async () => {
const names = [];
for await (const firstName of person.friends.friends(namedNode('http://ex.org/John')).firstName)
names.push(firstName);
expect(names.map(n => `${n}`)).toEqual(['Alice', 'Bob', 'Carol']);
});

it('returns true for an addition with 3 links', async () => {
expect(await person.friends.firstName.add('Ruben')).toBeTruthy();
});
Expand Down
23 changes: 22 additions & 1 deletion test/integration/sparql-test.js
Expand Up @@ -81,6 +81,27 @@ describe('a query path with a path expression handler', () => {
ORDER BY ASC(?v3) ASC(?givenName)`));
});

it('resolves a path with 3 links and fixed values', async () => {
const query = await person.friends.friends(namedNode('http://ex.org/Alice'), namedNode('http://ex.org/Bob')).name.sparql;
expect(query).toEqual(deindent(`
SELECT ?name WHERE {
<https://example.org/#me> <http://xmlns.com/foaf/0.1/knows> ?v0.
?v0 <http://xmlns.com/foaf/0.1/knows> <http://ex.org/Alice>, <http://ex.org/Bob>.
?v0 <http://xmlns.com/foaf/0.1/name> ?name.
}`));
});

it('resolves a path with 3 links and reversed fixed values', async () => {
const query = await person.friends.friendOf(namedNode('http://ex.org/Alice'), namedNode('http://ex.org/Bob')).name.sparql;
expect(query).toEqual(deindent(`
SELECT ?name WHERE {
<https://example.org/#me> <http://xmlns.com/foaf/0.1/knows> ?v0.
<http://ex.org/Alice> <http://xmlns.com/foaf/0.1/knows> ?v0.
<http://ex.org/Bob> <http://xmlns.com/foaf/0.1/knows> ?v0.
?v0 <http://xmlns.com/foaf/0.1/name> ?name.
}`));
});

it('resolves a path with 1 link and a predicates call', async () => {
const query = await person.predicates.sparql;
expect(query).toEqual(deindent(`
Expand Down Expand Up @@ -183,7 +204,7 @@ describe('a query path with a path expression handler', () => {

it('errors on a path with 3 links and an addition without args', async () => {
expect(() => person.friends.friends.add().sparql)
.toThrow(new Error('Mutation on [object Object] can not be invoked without arguments'));
.toThrow();
});

it('resolves a path with 3 links and an addition with a raw arg and path arg', async () => {
Expand Down
37 changes: 37 additions & 0 deletions test/unit/JSONLDResolver-test.js
Expand Up @@ -60,6 +60,43 @@ describe('a JSONLDResolver instance with a context', () => {
});
});

describe('resolving properties as functions', () => {
const pathData = { extendPath: jest.fn(x => Object.assign(x, { proxy: x })) };

let result;
beforeEach(() => result = resolver.resolve('knows', pathData));

it('exists', () => {
result = resolver.resolve('knows', pathData);
expect(result.asFunction).toBeInstanceOf(Function);
});

it('errors if there are no arguments', () => {
expect(() => result.asFunction(result, [])).toThrowError();
});

it('errors if the input is not an RDF object', () => {
expect(() => result.asFunction(result, ['Ruben'])).toThrowError();
});

describe('with 2 values', () => {
const ruben = namedNode('Ruben');
const joachim = namedNode('Joachim');
let applied;
beforeEach(() => {
applied = result.asFunction(result, [ruben, joachim]);
});

it('stores the new values in the result', () => {
expect(applied.values).toEqual([ruben, joachim]);
});

it('deletes the asFunction property', () => {
expect(applied).not.toHaveProperty('asFunction');
});
});
});

describe('resolving the foaf:knows property', () => {
const extendedPath = {};
const pathData = { extendPath: jest.fn(() => extendedPath) };
Expand Down
27 changes: 27 additions & 0 deletions test/unit/PathProxy-test.js
Expand Up @@ -19,6 +19,12 @@ describe('a PathProxy without handlers or resolvers', () => {
expect(path[Symbol('symbol')]).toBeUndefined();
});
});

describe('when calling it as a function', () => {
it('throws an error', () => {
expect(path()).toBeUndefined();
});
});
});
});

Expand Down Expand Up @@ -334,3 +340,24 @@ describe('a PathProxy whose paths are extended', () => {
});
});
});

describe('a PathProxy with an apply function', () => {
let pathProxy;
beforeAll(() => (pathProxy = new PathProxy()));

describe('a created path', () => {
let path, f;
beforeAll(() => {
f = jest.fn(x => x);
path = pathProxy.createPath({ asFunction: f });
});

it('calls the apply function', () => {
path('test');
expect(f).toBeCalledTimes(1);
const args = f.mock.calls[0];
expect(typeof args[0].asFunction).toBe('function');
expect(args[1]).toEqual(['test']);
});
});
});
42 changes: 42 additions & 0 deletions test/unit/SparqlHandler-test.js
Expand Up @@ -184,6 +184,48 @@ describe('a SparqlHandler instance', () => {
?v0 <https://ex.org/p2> ?p2.
}`));
});

it('supports fixed values', async () => {
const pathExpression = [
{ subject: namedNode('https://example.org/#me') },
{ predicate: namedNode('https://ex.org/p1') },
{ predicate: namedNode('https://ex.org/p2'), values: [namedNode('https://ex.org/o1'), namedNode('https://ex.org/o2')] },
];

const pathData = { property: 'p2' };
expect(await handler.handle(pathData, { pathExpression })).toEqual(deindent(`
SELECT ?v0 WHERE {
<https://example.org/#me> <https://ex.org/p1> ?v0.
?v0 <https://ex.org/p2> <https://ex.org/o1>, <https://ex.org/o2>.
}`));
});

it('errors on static triples in a query', async () => {
const pathExpression = [
{ subject: namedNode('https://example.org/#me') },
{ predicate: namedNode('https://ex.org/p1'), values: [namedNode('https://ex.org/o1'), namedNode('https://ex.org/o2')] },
{ predicate: namedNode('https://ex.org/p2') },
];

const pathData = { property: 'p2' };
await expect(handler.handle(pathData, { pathExpression })).rejects.toEqual(new Error('Can not have static objects if the subject is also static'));
});

it('supports reversed fixed values', async () => {
const pathExpression = [
{ subject: namedNode('https://example.org/#me') },
{ predicate: namedNode('https://ex.org/p1') },
{ predicate: namedNode('https://ex.org/p2'), values: [namedNode('https://ex.org/o1'), namedNode('https://ex.org/o2')], reverse: true },
];

const pathData = { property: 'p2' };
expect(await handler.handle(pathData, { pathExpression })).toEqual(deindent(`
SELECT ?v0 WHERE {
<https://example.org/#me> <https://ex.org/p1> ?v0.
<https://ex.org/o1> <https://ex.org/p2> ?v0.
<https://ex.org/o2> <https://ex.org/p2> ?v0.
}`));
});
});

describe('with mutationExpressions', () => {
Expand Down

0 comments on commit 11ccd86

Please sign in to comment.