Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for Union types. Solves #3 #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/SemanticGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { readFileSync } = require('fs');
const createRdfParser = require('n3').Parser;
const {
rdfIri, rdfsIri, owlIri, rdfsResource,
rdfType, rdfsLabel, rdfsComment, rdfsDomain, rdfsRange, rdfsSubClassOf, rdfsSubPropertyOf, owlInverseOf,
rdfType, rdfsLabel, rdfsComment, rdfsDomain, rdfsRange, rdfsSubClassOf, rdfsSubPropertyOf, owlInverseOf, owlUnionOf, rdfFirst, rdfRest
} = require('./constants');
const invariant = require('./utils/invariant');
const isIri = require('./utils/isIri');
Expand Down Expand Up @@ -34,6 +34,9 @@ const workingPredicates = [
rdfsSubClassOf,
rdfsSubPropertyOf,
owlInverseOf,
owlUnionOf,
rdfFirst,
rdfRest
];

parseFileAndIndex(baseGraph, '../ontologies/rdf.ttl');
Expand Down
4 changes: 4 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ x.rdfsSubClassOf = `${x.rdfsIri}subClassOf`;
x.rdfsSubPropertyOf = `${x.rdfsIri}subPropertyOf`;
x.owlFunctionalProperty = `${x.owlIri}FunctionalProperty`;
x.owlInverseOf = `${x.owlIri}inverseOf`;
x.owlUnionOf = `${x.owlIri}unionOf`;
x.rdfFirst = `${x.rdfIri}first`;
x.rdfRest = `${x.rdfIri}rest`;
x.rdfNil = `${x.rdfIri}nil`;

x._rdfType = `_${x.rdfType}`;
x._rdfsDomain = `_${x.rdfsDomain}`;
Expand Down
36 changes: 36 additions & 0 deletions src/graph/traversal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const warn = require('../utils/warn');
const { rdfsRange, owlUnionOf, rdfFirst, rdfRest, rdfNil } = require('../constants');

// Possible bug: stack overflow

Expand Down Expand Up @@ -36,7 +37,42 @@ function walklook(g, iri, walkIri, lookIri, s = new Set(), ws = new Set()) {
return s;
}

// TODO: Possible bug: stack overflow - using recursive function without debounce (_walkLinkedList).
// Resolves any resources that represent a Union of resources
// The resource must contain an owl:unionOf predicate to be considered a union resource.
// Additionally the object of the owl:unionOf must be an rdf linked list, having predicates rdf:first, rdf:rest and rdf:nil
// Inputs:
// g : graph
// resources : array[iri]
// returns: array[iri] after replacing all union iris with the resources in the union.
function resolveUnionResources (g, resources) {
function _walkLinkedList(listNode) {
const head = g[listNode][rdfFirst]
if (listNode != rdfNil && head && head != rdfNil) {
const tail = g[listNode][rdfRest];
if (tail && tail != rdfNil) {
return [head].concat(_walkLinkedList(tail));
}
return [head];
}
return [];
}

const unionResources = resources
.filter((iri) => g[iri][owlUnionOf])
// flatMap
.reduce((list, iri) => list.concat(g[iri][owlUnionOf]), [])
// flatMap
.reduce((list, listNode) => list.concat.apply(list,_walkLinkedList(listNode)), []);

const nonUnionResources = resources
.filter((iri) => !g[iri][owlUnionOf]);

return nonUnionResources.concat(unionResources).sort();
}

module.exports = {
walkmap,
walklook,
resolveUnionResources
};
6 changes: 3 additions & 3 deletions src/graphql/getGraphqlFieldConfig.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const { GraphQLList } = require('graphql');
const { xsdIri, rdfsLiteral, rdfsSubPropertyOf, rdfsRange } = require('../constants');
const { xsdIri, rdfsLiteral, rdfsSubPropertyOf, rdfsRange, owlUnionOf,rdfFirst,rdfRest,rdfNil } = require('../constants');
const warn = require('../utils/warn');
const { walklook } = require('../graph/traversal');
const { walklook, resolveUnionResources } = require('../graph/traversal');
const memorize = require('../graph/memorize');
const requireGraphqlRelay = require('../requireGraphqlRelay');
const isGraphqlList = require('./isGraphqlList');
Expand All @@ -21,7 +21,7 @@ function getGraphqlFieldConfig(g, iri) {
// Otherwise for each super-property, look for a range,
// if not found, check their super-properties and so on
// TODO: check walklook, maybe test it
const ranges = [...walklook(g, iri, rdfsSubPropertyOf, rdfsRange)];
const ranges = resolveUnionResources(g, [...walklook(g, iri, rdfsSubPropertyOf, rdfsRange)]);
const nRanges = ranges.length;

if (!nRanges) return;
Expand Down
63 changes: 59 additions & 4 deletions src/graphql/getGraphqlPolymorphicObjectType.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,64 @@
const { rdfsResource } = require('../constants');
const getGraphqlInterfaceType = require('./getGraphqlInterfaceType');
const memorize = require('../graph/memorize');
const { GraphQLUnionType } = require('graphql');
const getGraphqlName = require('./getGraphqlName');
const getGraphqlObjectType = require('./getGraphqlObjectType');

function getGraphqlPolymorphicObjectType(g/*, ranges*/) {
// TODO
return getGraphqlInterfaceType(g, rdfsResource);
// Generate an IRI that represents the Union of some resources
function getUnionIri(g, iris) {
const gqlNames = iris.map(x => getGraphqlName(g,x)).sort().join(',');
return `union:${gqlNames}`;
}

module.exports = getGraphqlPolymorphicObjectType;
// This adapter exists to compensate for the caller passing in a array of IRIs, and this breaks memorize.
// So the idea is we create an IRI that represents the array(ranges) and pass that to memorize.
// then we will receive that generated IRI in getGraphqlPolymorphicObjectType and have to
// reverse the process to get the ranges back from the generated IRI.
function memorizeRangesAdapter(fn, key) {
return (g, ranges) => {
const unionUri = getUnionIri(g, ranges);
// make sure the IRI for for this range is in the graph
g[unionUri] = g[unionUri] || {ranges:ranges};
// and pass the IRI through, instead of the ranges
return memorize(fn,key)(g,unionUri);
}
}

// Creates a GraphQLUnionType from a collection of iris in ranges.
// g : graph
// iri : resource name of the range
// ranges: array[resource:iri]
function getGraphQlUnionObjectType(g, iri, ranges) {
const types = ranges.map(x => getGraphqlObjectType(g,x)).sort();
const typeMap = types.reduce((a,c,i) => {
return Object.assign(a, {[c.name]: c});
}, {});
const gqlNames = ranges.map(x => getGraphqlName(g,x)).sort();
const unionName = gqlNames.join('_');

return new GraphQLUnionType({
name: `U_${unionName}`,
types: types,
description: `Union of ${gqlNames.join(' and ')}`,
resolveType : (value) => typeMap[value.type]
});
}

// Responsible for determining which type of GraphQlPolymorphicObject is used.
function getGraphqlPolymorphicObjectType(g, iri) {
const ranges = g[iri]["ranges"]; // this assumes memorizeRangesAdapter was used to wrap this call.
// Union strategy
if (ranges) {
return getGraphQlUnionObjectType(g, iri, ranges);
}
// TODO: other strategies.
return null;
}

module.exports = {
getUnionIri,
memorizeRangesAdapter,
getGraphQlUnionObjectType,
getGraphqlPolymorphicObjectType : memorizeRangesAdapter(getGraphqlPolymorphicObjectType, 'graphqlPolymorphicObjectType')
};
197 changes: 194 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ const commonTurtlePrefixes = require('./utils/commonTurtlePrefixes');
const castArrayShape = require('../src/utils/castArrayShape');
const isNil = require('../src/utils/isNil');
const capitalize = require('../src/utils/capitalize');
const { walkmap, walklook } = require('../src/graph/traversal');
const { rdfsClass, rdfType, _rdfsDomain } = require('../src/constants');
const { walkmap, walklook, resolveUnionResources } = require('../src/graph/traversal');
const { rdfsClass, rdfType, _rdfsDomain, rdfsRange, owlUnionOf, rdfFirst, rdfRest, rdfNil } = require('../src/constants');
const ArrayKeyedMap = require('../src/ArrayKeyedMap');
const SemanticGraph = require('..');

const { getUnionIri, getGraphqlPolymorphicObjectType} = require('../src/graphql/getGraphqlPolymorphicObjectType');
// TODO: split this file

// NOTE: getIriLocalName and isIri will be imported from an external lib someday
Expand Down Expand Up @@ -40,6 +40,112 @@ describe('Utils', () => {
});
});

describe('Graphql polymorphic Object Type', () => {

const graph = {
config: {
prefixes: { lllist:"list" }
},
a: {
x: ['b', 'c'],
y: ['d', 'e'],
},
b: {
x: ['a', 'c', 'd'],
z: ['a', 'f'],
},
c: {
x: ['c'],
},
d: {
y: ['a', 'b'],
z: ['c'],
},
e: {
z: ['c', 'd', 'g'],
},
f: {
x: ['g'],
},
g: {
y: ['a', 'f'],
},
h: {
[rdfsRange]: ['union:resource:1']
},
i: {
[rdfsRange]: ['union:resource:2']
},
'union:resource:empty': {
[owlUnionOf]: ['_:list:empty:node:1']
},
'_:list:empty:node:1': {
[rdfFirst]: [rdfNil],
[rdfRest]: [rdfNil]
},
'union:resource:single': {
[owlUnionOf]: ['_:list:_:node:1']
},
'_:list:_:node:1': {
[rdfFirst]: ['list:_:item:1'],
[rdfRest]: [rdfNil]
},
'union:resource:double': {
[owlUnionOf]: ['_:list:a:node:1']
},
'_:list:a:node:1': {
[rdfFirst]: ['list:a:item:1'],
[rdfRest]: ['_:list:a:node:2']
},
'_:list:a:node:2': {
[rdfFirst]: ['list:a:item:2'],
[rdfRest]: [rdfNil]
},
'union:resource:quad': {
[owlUnionOf]: ['_:list:b:node:1']
},
'_:list:b:node:1': {
[rdfFirst]: ['list:b:item:1'],
[rdfRest]: ['_:list:b:node:2']
},
'_:list:b:node:2': {
[rdfFirst]: ['list:b:item:2'],
[rdfRest]: '_:list:b:node:3'
},
'_:list:b:node:3': {
[rdfFirst]: ['list:b:item:3'],
[rdfRest]: '_:list:b:node:4'
},
'_:list:b:node:4': {
[rdfFirst]: ['list:b:item:4'],
[rdfRest]: [rdfNil]
},
'list:b;item;1': {
a: ['a']
},
'list:b;item;2': {
a: ['a']
},
'list:b;item;3': {
a: ['a']
},
};

it('Should Create a Union IRI from a range of resources', () => {
const unionIri = getUnionIri(graph, ['list:b;item;1','list:b;item;2','list:b;item;3']);
assert.deepEqual(unionIri, 'union:B_item_1,B_item_2,B_item_3');
})

it('Should Create a union type when given a range in IRI position', () => {
const unionResources = ['list:b;item;1','list:b;item;2','list:b;item;3'];
var createdType;
assert.doesNotThrow(() => {
createdType = getGraphqlPolymorphicObjectType(graph, unionResources);
});
assert.deepEqual(createdType._typeConfig.types.length, unionResources.length);
});
})

describe('Graph traversal', () => {

const graph = {
Expand Down Expand Up @@ -67,6 +173,56 @@ describe('Graph traversal', () => {
g: {
y: ['a', 'f'],
},
h: {
[rdfsRange]: ['union:resource:1']
},
i: {
[rdfsRange]: ['union:resource:2']
},
'union:resource:empty': {
[owlUnionOf]: ['_:list:empty:node:1']
},
'_:list:empty:node:1': {
[rdfFirst]: [rdfNil],
[rdfRest]: [rdfNil]
},
'union:resource:single': {
[owlUnionOf]: ['_:list:_:node:1']
},
'_:list:_:node:1': {
[rdfFirst]: ['list:_:item:1'],
[rdfRest]: [rdfNil]
},
'union:resource:double': {
[owlUnionOf]: ['_:list:a:node:1']
},
'_:list:a:node:1': {
[rdfFirst]: ['list:a:item:1'],
[rdfRest]: ['_:list:a:node:2']
},
'_:list:a:node:2': {
[rdfFirst]: ['list:a:item:2'],
[rdfRest]: [rdfNil]
},
'union:resource:quad': {
[owlUnionOf]: ['_:list:b:node:1']
},
'_:list:b:node:1': {
[rdfFirst]: ['list:b:item:1'],
[rdfRest]: ['_:list:b:node:2']
},
'_:list:b:node:2': {
[rdfFirst]: ['list:b:item:2'],
[rdfRest]: '_:list:b:node:3'
},
'_:list:b:node:3': {
[rdfFirst]: ['list:b:item:3'],
[rdfRest]: '_:list:b:node:4'
},
'_:list:b:node:4': {
[rdfFirst]: ['list:b:item:4'],
[rdfRest]: [rdfNil]
}
};

it('walkmap', () => {
Expand All @@ -88,6 +244,41 @@ describe('Graph traversal', () => {
assert.deepEqual([...walklook(graph, 'g', 'x', 'z')], []);
assert.deepEqual([...walklook(graph, 'c', 'x', 'z')], []);
});

it('resolves UnionResources that are empty unions', () => {
// Resolves any resources that represent a Union of resources
// The resource must contain an owl:unionOf predicate to be considered a union resource.
// Additionally the object of the owl:unionOf must be an rdf linked list, having predicates rdf:first, rdf:rest and rdf:nil
assert.deepEqual(resolveUnionResources(graph, ['union:resource:empty']), []);
});

it('resolves UnionResources that contain 1 item', () => {
// Resolves any resources that represent a Union of resources
// The resource must contain an owl:unionOf predicate to be considered a union resource.
// Additionally the object of the owl:unionOf must be an rdf linked list, having predicates rdf:first, rdf:rest and rdf:nil
assert.deepEqual(resolveUnionResources(graph, ['union:resource:single']), ['list:_:item:1']);
});

it('resolves UnionResources that contain 2 items', () => {
// Resolves any resources that represent a Union of resources
// The resource must contain an owl:unionOf predicate to be considered a union resource.
// Additionally the object of the owl:unionOf must be an rdf linked list, having predicates rdf:first, rdf:rest and rdf:nil
assert.deepEqual(resolveUnionResources(graph, ['union:resource:double']), ['list:a:item:1','list:a:item:2']);
});

it('resolves UnionResources that contain 4 items', () => {
// Resolves any resources that represent a Union of resources
// The resource must contain an owl:unionOf predicate to be considered a union resource.
// Additionally the object of the owl:unionOf must be an rdf linked list, having predicates rdf:first, rdf:rest and rdf:nil
assert.deepEqual(resolveUnionResources(graph, ['union:resource:quad']), ['list:b:item:1','list:b:item:2','list:b:item:3','list:b:item:4']);
});

it('resolves UnionResources that contain [0 && 1 && 2 && 4] items', () => {
// Resolves any resources that represent a Union of resources
// The resource must contain an owl:unionOf predicate to be considered a union resource.
// Additionally the object of the owl:unionOf must be an rdf linked list, having predicates rdf:first, rdf:rest and rdf:nil
assert.deepEqual(resolveUnionResources(graph, ['union:resource:empty','union:resource:single','union:resource:double','union:resource:quad']), ['list:_:item:1','list:a:item:1','list:a:item:2','list:b:item:1','list:b:item:2','list:b:item:3','list:b:item:4']);
});
});

describe('ArrayKeyedMap', () => {
Expand Down