Skip to content

Commit 413e924

Browse files
committed
first function handler test works
1 parent ee4386c commit 413e924

20 files changed

+3400
-19
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Function Handler (JS implementation)
22

3+
The function handler tries to be a PoC SDK for FnO function descriptions,
4+
and interact with a Function Hub instance.
5+
36
## Installation
47

58
`npm install`

package-lock.json

+2,780-17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+23-2
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,42 @@
44
"description": "FnO Function Handler",
55
"main": "example-client.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"test": "ts-mocha 'src/**/*.test.ts'",
8+
"build:typescript": "npx tsc index.ts"
89
},
910
"author": "Lander Noterman <[email protected]>",
1011
"license": "ISC",
1112
"dependencies": {
13+
"@types/rdf-js": "^4.0.0",
14+
"acorn": "^6.2.0",
1215
"append-string": "^1.0.2",
1316
"componentsjs": "^3.2.0",
1417
"concat-stream": "^1.6.0",
1518
"crypto-js": "^3.1.9-1",
19+
"graphy": "^4.3.3",
1620
"indent-string": "^3.2.0",
1721
"jsonld": "^1.5.1",
22+
"ldfetch": "^1.2.3",
1823
"left-pad": "^1.3.0",
1924
"mkdirp": "^0.5.1",
25+
"n3": "^1.1.1",
2026
"npm-programmatic": "0.0.10",
27+
"php-parser": "^3.0.0-prerelease.8",
28+
"rdflib": "^2.1.6",
2129
"request": "^2.88.0",
22-
"request-promise": "^4.2.2"
30+
"request-promise": "^4.2.2",
31+
"tslint-config-airbnb": "^5.11.2"
32+
},
33+
"devDependencies": {
34+
"@types/acorn": "^4.0.6",
35+
"@types/chai": "^4.2.14",
36+
"@types/mocha": "^8.2.0",
37+
"@types/node": "^16.9.1",
38+
"chai": "^4.2.0",
39+
"mocha": "^8.2.1",
40+
"ts-mocha": "^8.0.0",
41+
"ts-node": "^9.1.1",
42+
"tslint": "^6.1.3",
43+
"typescript": "^4.1.3"
2344
}
2445
}

src/FunctionHandler.test.ts

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { expect } from 'chai';
2+
import { FunctionHandler } from "./FunctionHandler";
3+
import { } from 'mocha';
4+
import { JavaScriptHandler } from './handlers/JavaScriptHandler';
5+
import { Argument, Function, Implementation } from './models';
6+
import { namedNode } from 'rdflib';
7+
import { Term } from 'rdf-js';
8+
import prefixes from './prefixes';
9+
10+
const fnTtl = `@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
11+
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
12+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
13+
@prefix ex: <http://www.example.com#> .
14+
@prefix dcterms: <http://purl.org/dc/terms/> .
15+
@prefix doap: <http://usefulinc.com/ns/doap#> .
16+
@prefix fno: <https://w3id.org/function/ontology#> .
17+
@prefix fnoi: <https://w3id.org/function/vocabulary/implementation#> .
18+
@prefix fnom: <https://w3id.org/function/vocabulary/mapping#> .
19+
@prefix fns: <http://example.com/functions#> .
20+
21+
fns:aParameter rdf:type fno:Parameter ;
22+
rdfs:label "a"@en ;
23+
fno:predicate fns:a ;
24+
fno:required "true"^^xsd:boolean .
25+
26+
fns:bParameter rdf:type fno:Parameter ;
27+
rdfs:label "b"@en ;
28+
fno:predicate fns:b ;
29+
fno:required "true"^^xsd:boolean .
30+
31+
fns:sumOutput rdf:type fno:Output ;
32+
rdfs:label "sum output"@en ;
33+
fno:predicate fno:Result .
34+
35+
fns:sum rdf:type fno:Function ;
36+
dcterms:description "Description of the sum function"@en ;
37+
rdfs:label "sum"@en ;
38+
fno:expects _:g0 ;
39+
fno:returns _:g2 .
40+
41+
_:g0 rdf:first fns:aParameter ;
42+
rdf:rest _:g1 .
43+
44+
_:g1 rdf:first fns:bParameter ;
45+
rdf:rest rdf:nil .
46+
47+
_:g2 rdf:first fns:sumOutput ;
48+
rdf:rest rdf:nil .
49+
50+
fns:sumImplementation rdf:type fno:Implementation, fnoi:JavaScriptImplementation, fnoi:JavaScriptFunction ;
51+
doap:release fns:sumImplementationRelease .
52+
53+
fns:sumImplementationRelease doap:file-release fns:sumImplementationReleaseFile .
54+
55+
fns:sumImplementationReleaseFile ex:value " function sum(a, b) { return a + b; } " .
56+
57+
fns:sumMapping rdf:type fno:Mapping ;
58+
fno:function fns:sum ;
59+
fno:implementation fns:sumImplementation ;
60+
fno:parameterMapping fns:aParametermapping, fns:bParametermapping ;
61+
fno:returnMapping fns:sumReturnMapping ;
62+
fno:methodMapping fns:sumMethodMapping .
63+
64+
fns:aParametermapping rdf:type fno:ParameterMapping, fnom:PositionParameterMapping ;
65+
fnom:functionParameter fns:aParameter ;
66+
fnom:implementationParameterPosition "0"^^xsd:integer .
67+
68+
fns:bParametermapping rdf:type fno:ParameterMapping, fnom:PositionParameterMapping ;
69+
fnom:functionParameter fns:bParameter ;
70+
fnom:implementationParameterPosition "1"^^xsd:integer .
71+
72+
fns:sumReturnMapping rdf:type fno:ReturnMapping, fnom:DefaultReturnMapping .
73+
74+
fns:sumMethodMapping rdf:type fno:MethodMapping, fnom:StringMethodMapping ;
75+
fnom:method-name "sum" .
76+
`;
77+
78+
describe('FunctionHandler tests', () => { // the tests container
79+
it.skip('can parse a function file', async () => { // the single test
80+
const handler = new FunctionHandler();
81+
await handler.addFunctionResource("http://users.ugent.be/~bjdmeest/function/grel.ttl#");
82+
83+
const fn = await handler.getFunction("http://users.ugent.be/~bjdmeest/function/grel.ttl#array_join");
84+
85+
expect(fn).to.be.any;
86+
});
87+
88+
it('can load a local file, add a handler, and execute a function', async () => {
89+
const handler = new FunctionHandler();
90+
await handler.addFunctionResource(`${prefixes.fns}sum`, {
91+
type: 'string',
92+
contents: fnTtl,
93+
contentType: "text/turtle"
94+
});
95+
const fn = await handler.getFunction(`${prefixes.fns}sum`);
96+
97+
expect(fn).to.be.not.null;
98+
99+
expect(fn.id).to.equal(`${prefixes.fns}sum`)
100+
101+
const jsHandler = new JavaScriptHandler();
102+
jsHandler.loadFunction(new Implementation(namedNode(`${prefixes.fns}sumImplementation`) as Term), (a, b) => a + b)
103+
104+
handler.addHandler(jsHandler);
105+
const result = await handler.executeFunction(fn, [
106+
new Argument(`${prefixes.fns}aParameter`, 1),
107+
new Argument(`${prefixes.fns}bParameter`, 1),
108+
])
109+
110+
expect(result).to.equal(2);
111+
return;
112+
})
113+
});

src/FunctionHandler.ts

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { Argument, Function, Implementation, Mapping, Predicate } from './models';
2+
import { GraphHandler, LocalValue } from "./GraphHandler";
3+
import { Handler } from './handlers/Handler';
4+
import * as $rdf from "rdflib";
5+
6+
import prefixes from './prefixes';
7+
8+
export class FunctionHandler {
9+
private _graphHandler;
10+
private _handlerIndex = {};
11+
12+
constructor() {
13+
this._graphHandler = new GraphHandler();
14+
}
15+
16+
async addFunctionResource(iri: string, localValue: LocalValue | null = null) {
17+
await this._graphHandler.addGraph(iri, localValue)
18+
};
19+
20+
async addHandler(handler: Handler) {
21+
this._handlerIndex[handler.id] = handler;
22+
}
23+
24+
async getFunction(iri: string): Promise<Function | null> {
25+
const term = this._graphHandler.getSubjectOfType(iri, `${prefixes.fno}Function`);
26+
if (term) {
27+
return new Function(term);
28+
}
29+
return null;
30+
}
31+
32+
/**
33+
* @deprecated
34+
* @param iri
35+
* @returns
36+
*/
37+
async getPredicate(iri: string) {
38+
const term = this._graphHandler.getSubjectOfType(iri);
39+
if (term) {
40+
return new Predicate(term);
41+
}
42+
return null;
43+
}
44+
45+
async executeFunction(fn: Function, args: Argument[], mapping: Mapping | null = null) {
46+
let mappings: Mapping[] = [];
47+
if (mapping === null) {
48+
mappings = this.getMappingsFromFunction(fn);
49+
} else {
50+
mappings = [mapping];
51+
}
52+
const possibleImplementations: {
53+
mapping: Mapping,
54+
implementation: Implementation,
55+
handler: Handler
56+
}[] = [];
57+
if (mappings.length === 0) {
58+
console.warn(`Could not find any relevant mapping for function ${fn.id}`)
59+
}
60+
mappings.forEach(mapping => {
61+
const implementations = this.getImplementationFromMapping(mapping);
62+
if (implementations.length === 0) {
63+
console.warn(`Could not find any relevant implementation for mapping ${mapping.id}`)
64+
return;
65+
}
66+
implementations.forEach(implementation => {
67+
const handlers = this.getHandlers(implementation);
68+
if (handlers.length === 0) {
69+
console.warn(`Could not find any relevant handlers for implementation ${implementation.id}`)
70+
return;
71+
}
72+
handlers.forEach(handler => {
73+
possibleImplementations.push({
74+
mapping,
75+
implementation,
76+
handler
77+
})
78+
})
79+
})
80+
})
81+
if (possibleImplementations.length === 0) {
82+
throw new Error(`Could not find any relevant implementation to execute ${fn.id}`)
83+
}
84+
const optimalImplementation = possibleImplementations[0];
85+
return optimalImplementation.handler.executeFunction(optimalImplementation.implementation, this.getArgs(optimalImplementation.mapping, args))
86+
}
87+
88+
private getMappingsFromFunction(fn: Function) {
89+
const mappings = this._graphHandler.match(null, $rdf.sym(`${prefixes.fno}function`), fn.term);
90+
if (mappings.length === 0) {
91+
return [];
92+
}
93+
return mappings.map(m => new Mapping(m.subject));
94+
}
95+
96+
private getImplementationFromMapping(mapping: Mapping) {
97+
const implementations = this._graphHandler.match(mapping.term, $rdf.sym(`${prefixes.fno}implementation`));
98+
if (implementations.length === 0) {
99+
return [];
100+
}
101+
return implementations.map(m => new Implementation(m.object));
102+
}
103+
104+
private getHandlers(implementation: Implementation) {
105+
const loadedHandlerClasses = Object.keys(this._handlerIndex);
106+
const handlers: Handler[] = [];
107+
loadedHandlerClasses.forEach(c => {
108+
const match = this._graphHandler.match(implementation.term, $rdf.sym(`${prefixes.rdf}type`), $rdf.sym(c))
109+
if(match.length > 0) {
110+
handlers.push(this._handlerIndex[c]);
111+
}
112+
})
113+
114+
return handlers;
115+
}
116+
117+
private getArgs(mapping: Mapping, args: Argument[]) {
118+
const result = {};
119+
const parameterMappings = this._graphHandler.match(mapping.term, $rdf.sym(`${prefixes.fno}parameterMapping`)).map(p => p.object)
120+
parameterMappings.forEach(pMapping => {
121+
let parameter = this._graphHandler.match(pMapping, $rdf.sym(`${prefixes.fnom}functionParameter`)).map(p=>p.object);
122+
if (parameter.length === 0) {
123+
console.warn(`Could not find parameter assigned to ${pMapping.value}`)
124+
return
125+
}
126+
if (parameter.length > 1) {
127+
console.warn(`More parameters for ${pMapping.value} than expected (1). Picking one at random.`)
128+
}
129+
parameter = parameter[0];
130+
const arg = args.filter(a => parameter.value === a.term.value);
131+
if (!arg) {
132+
console.warn(`Argument for parameter ${parameter.value} not found`)
133+
return
134+
}
135+
let type = this._graphHandler.match(parameter, $rdf.sym(`${prefixes.fno}type`));
136+
if (type.length === 0) {
137+
console.warn(`No type information for parameter ${parameter.value} found`)
138+
}
139+
if(type.length > 1) {
140+
console.warn(`More types for ${parameter.value} than expected (1). Picking one at random.`)
141+
}
142+
type = type[0] || null;
143+
if (this._graphHandler.match(pMapping, $rdf.sym(`${prefixes.rdf}type`), $rdf.sym(`${prefixes.fnom}PositionParameterMapping`)).length > 0) {
144+
const positions = this._graphHandler.match(pMapping, $rdf.sym(`${prefixes.fnom}implementationParameterPosition`)).map(p => p.object.value);
145+
positions.forEach(p => {
146+
arg.forEach(a => {
147+
addToResult(p, a.value, type);
148+
})
149+
})
150+
} else {
151+
throw new Error('Unsupported if not positionparametermapping')
152+
}
153+
})
154+
155+
return result;
156+
157+
function addToResult(key, value, type) {
158+
if (type?.value === `${prefixes.rdf}List`) {
159+
if (!result[key]) {
160+
result[key] = [];
161+
}
162+
result[key].push(value);
163+
} else {
164+
if (!result[key]) {
165+
result[key] = value;
166+
} else {
167+
console.warn(`Multiple values found for argument ${key}. Keeping a random one.`)
168+
}
169+
}
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)