diff --git a/packages/core/lib/resourceShape.ts b/packages/core/lib/resourceShape.ts index 26e3d45..1a610b2 100644 --- a/packages/core/lib/resourceShape.ts +++ b/packages/core/lib/resourceShape.ts @@ -6,18 +6,20 @@ import type { Kopflos, KopflosResponse } from './Kopflos.js' interface ResourceShapeDirectMatch { api: NamedNode resourceShape: NamedNode + subject: NamedNode } interface ResourceShapeTypeMatch { api: NamedNode resourceShape: NamedNode + subject: NamedNode } interface ResourceShapeObjectMatch { api: NamedNode resourceShape: NamedNode property: NamedNode - subject: NamedNode + object: NamedNode } export type ResourceShapeMatch = ResourceShapeDirectMatch | ResourceShapeTypeMatch | ResourceShapeObjectMatch @@ -30,8 +32,7 @@ const __dirname = path.dirname(new URL(import.meta.url).pathname) const select = fs.readFileSync(path.resolve(__dirname, '../query/resourceShapes.rq')).toString() export default async (iri: NamedNode, instance: Kopflos) => { - return instance.env.sparql.default.parsed.query.select(` - ${select} - VALUES ?resource { <${iri.value}> } - `) as unknown as Promise + return instance.env.sparql.default.parsed.query.select( + select.replace('sh:this', `<${iri.value}>`), + ) as unknown as Promise } diff --git a/packages/core/query/resourceShapes.rq b/packages/core/query/resourceShapes.rq index c867edf..4bf1468 100644 --- a/packages/core/query/resourceShapes.rq +++ b/packages/core/query/resourceShapes.rq @@ -2,23 +2,30 @@ PREFIX rdfs: PREFIX sh: prefix kopflos: -SELECT distinct ?api ?resource ?parent ?resourceShape ?property { +SELECT * { { - ?resourceShape a kopflos:ResourceShape . - ?resourceShape kopflos:api ?api . + SELECT ?api ?resourceShape ?subject ?property ?object { + { + ?resourceShape a kopflos:ResourceShape . + ?resourceShape kopflos:api ?api . + + { + ?resourceShape sh:targetClass ?targetClass . + ?type rdfs:subClassOf* ?targetClass . + ?subject a ?type . + } UNION { + ?resourceShape sh:targetNode ?subject . + } + } + + optional { + ?resourceShape sh:property/sh:path ?property . + ?subject ?property ?object . + } - { - ?resourceShape sh:targetClass ?targetClass . - ?type rdfs:subClassOf* ?targetClass . - ?candidate a ?type . - } UNION { - ?resourceShape sh:targetNode ?candidate . } } - OPTIONAL { - ?resourceShape sh:property/sh:path ?property . - ?candidate ?property ?resource . - BIND(?candidate as ?parent) - } + VALUES ?resource { sh:this } + FILTER(?resource = ?subject || ?resource = ?object) } diff --git a/packages/core/test/lib/Kopflos.test.ts b/packages/core/test/lib/Kopflos.test.ts index 965f27a..1f6d753 100644 --- a/packages/core/test/lib/Kopflos.test.ts +++ b/packages/core/test/lib/Kopflos.test.ts @@ -46,6 +46,7 @@ describe('lib/Kopflos', () => { resourceShapeLookup: async () => [{ api: ex.api, resourceShape: ex.Shape, + subject: ex.foo, }], resourceLoaderLookup: async () => undefined, }) @@ -66,6 +67,7 @@ describe('lib/Kopflos', () => { resourceShapeLookup: async () => [{ api: ex.api, resourceShape: ex.FooShape, + subject: ex.foo, }], handlerLookup: async () => undefined, }) @@ -86,6 +88,7 @@ describe('lib/Kopflos', () => { resourceShapeLookup: async () => [{ api: ex.api, resourceShape: ex.FooShape, + subject: ex.foo, }], handlerLookup: async () => async () => ({ status: 200, diff --git a/packages/core/test/lib/resourceShape.test.ts b/packages/core/test/lib/resourceShape.test.ts index 705d87b..59198e2 100644 --- a/packages/core/test/lib/resourceShape.test.ts +++ b/packages/core/test/lib/resourceShape.test.ts @@ -20,83 +20,118 @@ describe('lib/resourceShape', () => { }) describe('default resource shape lookup', () => { - it('finds directly matching resource', async () => { - // given - const kopflos = new Kopflos(rdf.clownface(), options) - - // when - const results = await defaultResourceShapeLookup(ex.bar, kopflos) - - // then - expect(results[0]).to.deep.contain({ - api: ex.api, - resourceShape: ex.barShape, + context('when directly matching resource shape is available', () => { + it('is found by resource IRI', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) + + // when + const results = await defaultResourceShapeLookup(ex.bar, kopflos) + + // then + expect(results[0]).to.deep.contain({ + api: ex.api, + resourceShape: ex.barShape, + subject: ex.bar, + }) + expect(results).to.have.length(1) }) - expect(results).to.have.length(1) - }) - it('finds matching resource by type', async () => { - // given - const kopflos = new Kopflos(rdf.clownface(), options) + it('does not find anything when requested resource does not match that shape', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) - // when - const results = await defaultResourceShapeLookup(ex.foo, kopflos) + // when + const results = await defaultResourceShapeLookup(ex.boo, kopflos) - // then - expect(results[0]).to.deep.contain({ - api: ex.api, - resourceShape: ex.FooShape, + // then + expect(results).to.be.empty }) - expect(results).to.have.length(1) }) - it('finds matching resource by super type', async () => { - // given - const kopflos = new Kopflos(rdf.clownface(), options) + context('when class targeting resource shape is available', () => { + it('is found when requested resource has that type', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) + + // when + const results = await defaultResourceShapeLookup(ex.foo, kopflos) + + // then + expect(results[0]).to.deep.contain({ + api: ex.api, + resourceShape: ex.FooShape, + subject: ex.foo, + }) + expect(results).to.have.length(1) + }) + + it('is found when requested resource has derived type', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) - // when - const results = await defaultResourceShapeLookup(ex.baz, kopflos) + // when + const results = await defaultResourceShapeLookup(ex.baz, kopflos) - // then - expect(results[0]).to.deep.contain({ - api: ex.api, - resourceShape: ex.FooShape, + // then + expect(results[0]).to.deep.contain({ + api: ex.api, + resourceShape: ex.FooShape, + subject: ex.baz, + }) + expect(results).to.have.length(1) }) - expect(results).to.have.length(1) - }) - it('finds matching resource by property usage', async () => { - // given - const kopflos = new Kopflos(rdf.clownface(), options) + it('find nothing when a resource exists but has a different type', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) - // when - const results = await defaultResourceShapeLookup(ex['foo/location'], kopflos) + // when + const results = await defaultResourceShapeLookup(ex.xyz, kopflos) - // then - expect(results[0]).to.deep.contain({ - api: ex.api, - resourceShape: ex.FooShape, - parent: ex.foo, - property: rdf.ns.schema.location, + // then + expect(results).to.be.empty }) - expect(results).to.have.length(1) }) - it('finds matching resource by property usage of resource shape with target node', async () => { - // given - const kopflos = new Kopflos(rdf.clownface(), options) - - // when - const results = await defaultResourceShapeLookup(ex['foo/location'], kopflos) + context('when class targeting resource shapes has property shape', () => { + it('finds matching resource by property usage of class targeting shape', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) + + // when + const results = await defaultResourceShapeLookup(ex['foo/location'], kopflos) + + // then + expect(results[0]).to.deep.contain({ + api: ex.api, + resourceShape: ex.FooShape, + subject: ex.foo, + object: ex['foo/location'], + property: rdf.ns.schema.location, + }) + expect(results).to.have.length(1) + }) + }) - // then - expect(results[0]).to.deep.contain({ - api: ex.api, - resourceShape: ex.FooShape, - parent: ex.foo, - property: rdf.ns.schema.location, + context('when node targeting resource shapes has property shape', () => { + it('finds matching resource by property usage of resource shape with target node', async () => { + // given + const kopflos = new Kopflos(rdf.clownface(), options) + + // when + const results = await defaultResourceShapeLookup(ex['foo/location'], kopflos) + + // then + expect(results[0]).to.deep.contain({ + api: ex.api, + resourceShape: ex.FooShape, + subject: ex.foo, + object: ex['foo/location'], + property: rdf.ns.schema.location, + }) + expect(results).to.have.length(1) }) - expect(results).to.have.length(1) }) }) }) diff --git a/packages/core/test/lib/resourceShape.test.ts.trig b/packages/core/test/lib/resourceShape.test.ts.trig index edfd337..ed70530 100644 --- a/packages/core/test/lib/resourceShape.test.ts.trig +++ b/packages/core/test/lib/resourceShape.test.ts.trig @@ -4,7 +4,7 @@ PREFIX sh: PREFIX kopflos: PREFIX ex: -GRAPH { +GRAPH { # api ex:api a kopflos:Api . @@ -15,25 +15,13 @@ GRAPH { +GRAPH { # resources ex:foo a ex:Foo . - - # api - ex:api a kopflos:Api . - - ex:FooShape - a kopflos:ResourceShape ; - sh:targetClass ex:Foo ; - kopflos:api ex:api ; - . -} - -GRAPH { -# resources ex:baz a ex:Baz . ex:Baz rdfs:subClassOf ex:Bar . ex:Bar rdfs:subClassOf ex:Foo . + ex:xyz a ex:Xyz . # api ex:api a kopflos:Api . @@ -45,7 +33,7 @@ GRAPH { +GRAPH { # resources ex:foo a ex:Foo ; @@ -63,7 +51,7 @@ GRAPH { +GRAPH { # resources ex:foo schema:location ex:foo\/location . diff --git a/packages/core/test/support/testData.ts b/packages/core/test/support/testData.ts index 37290f8..3800d64 100644 --- a/packages/core/test/support/testData.ts +++ b/packages/core/test/support/testData.ts @@ -88,7 +88,7 @@ export function createStore(base: string, options: Options = {}) { } function testGraph(test: Mocha.Test): NamedNode { - return rdf.namedNode(encodeURI(test.titlePath().map(removeSpaces).join('/'))) + return rdf.namedNode(encodeURI(test.titlePath().slice(1, -1).map(removeSpaces).join('/'))) } function removeSpaces(arg: string) {