diff --git a/src/SemanticGraph.js b/src/SemanticGraph.js index 2ade09b..c0190f0 100644 --- a/src/SemanticGraph.js +++ b/src/SemanticGraph.js @@ -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'); @@ -34,6 +34,9 @@ const workingPredicates = [ rdfsSubClassOf, rdfsSubPropertyOf, owlInverseOf, + owlUnionOf, + rdfFirst, + rdfRest ]; parseFileAndIndex(baseGraph, '../ontologies/rdf.ttl'); diff --git a/src/constants.js b/src/constants.js index 368e402..4d0827a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -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}`; diff --git a/src/graph/traversal.js b/src/graph/traversal.js index cdb66ce..119290d 100644 --- a/src/graph/traversal.js +++ b/src/graph/traversal.js @@ -1,4 +1,5 @@ const warn = require('../utils/warn'); +const { rdfsRange, owlUnionOf, rdfFirst, rdfRest, rdfNil } = require('../constants'); // Possible bug: stack overflow @@ -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 }; diff --git a/src/graphql/getGraphqlFieldConfig.js b/src/graphql/getGraphqlFieldConfig.js index c930615..11e6f9f 100644 --- a/src/graphql/getGraphqlFieldConfig.js +++ b/src/graphql/getGraphqlFieldConfig.js @@ -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'); @@ -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; diff --git a/src/graphql/getGraphqlPolymorphicObjectType.js b/src/graphql/getGraphqlPolymorphicObjectType.js index a6fab3d..1bfb8fb 100644 --- a/src/graphql/getGraphqlPolymorphicObjectType.js +++ b/src/graphql/getGraphqlPolymorphicObjectType.js @@ -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') +}; diff --git a/test/index.js b/test/index.js index aa816c0..7951928 100644 --- a/test/index.js +++ b/test/index.js @@ -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 @@ -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 = { @@ -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', () => { @@ -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', () => {