diff --git a/lib/CBDShapeExtractor.ts b/lib/CBDShapeExtractor.ts index a685fe2..112af81 100644 --- a/lib/CBDShapeExtractor.ts +++ b/lib/CBDShapeExtractor.ts @@ -57,12 +57,6 @@ export class CBDShapeExtractor { } } - loadQuadStreamInStore(store: RdfStore, quadStream: any) { - return new Promise((resolve, reject) => { - store.import(quadStream).on("end", resolve).on("error", reject); - }); - } - public async bulkExtract( store: RdfStore, ids: Array, @@ -128,8 +122,6 @@ export class CBDShapeExtractor { shapeId?: Term, graphsToIgnore?: Array, ): Promise> { - const logger = log.extend("extract"); - // First extract everything except for something within the graphs to ignore, or within the graph of the current entity, as that’s going to be added anyway later on let dontExtractFromGraph: Array = ( graphsToIgnore ? graphsToIgnore : [] @@ -137,104 +129,207 @@ export class CBDShapeExtractor { return item.value; }); - const dereferenced: string[] = []; - const dereferenceAndRetry: ( - target: string, - msg?: string, - ) => Promise = async (target: string, msg?: string) => { - if (dereferenced.indexOf(target) == -1) { - logger(`Dereferencing ${target} ${msg ?? ""}`); - dereferenced.push(target); - await this.loadQuadStreamInStore( - store, - ( - await this.dereferencer.dereference(target, { - fetch: this.options.fetch, - }) - ).data, - ); + const extractInstance = new ExtractInstance( + store, + this.dereferencer, + dontExtractFromGraph, + this.options, + this.shapesGraph, + ); - return await tryExtract(); - } else { - throw "Already dereferenced " + target + " won't dereference again"; + return await extractInstance.extract(id, false, shapeId); + } +} + +export type Extracted = { + forwards: { + [node: string]: Extracted; + }; + backwards: { + [node: string]: Extracted; + }; +}; + +export type ExtractReasons = { + cbd: boolean; + shape: boolean; +}; + +export class CbdExtracted { + topology: Extracted; + cbdExtractedMap: RDFMap; + + constructor( + topology?: Extracted, + cbdExtracted: RDFMap = new RDFMap(), + ) { + if (topology) { + this.topology = topology; + } else { + this.topology = { forwards: {}, backwards: {} }; + } + this.cbdExtractedMap = cbdExtracted; + } + + addCBDTerm(term: Term) { + const t = this.cbdExtractedMap.get(term); + if (t) { + t.cbd = true; + } else { + this.cbdExtractedMap.set(term, { cbd: true, shape: false }); + } + } + + addShapeTerm(term: Term) { + const t = this.cbdExtractedMap.get(term); + if (t) { + t.shape = true; + } else { + this.cbdExtractedMap.set(term, { cbd: true, shape: false }); + } + } + + cbdExtracted(term: Term): boolean { + return !!this.cbdExtractedMap.get(term)?.shape; + } + + push(term: Term, inverse: boolean): CbdExtracted { + if (inverse) { + if (!this.topology.backwards[term.value]) { + const ne: Extracted = { + forwards: {}, + backwards: {}, + }; + ne.forwards[term.value] = this.topology; + this.topology.backwards[term.value] = ne; } - }; - - const tryExtract: () => Promise = async () => { - const result: Quad[] = []; - try { - const cbdExtracted = new CbdExtracted(); - await this.extractRecursively( - store, + return new CbdExtracted( + this.topology.backwards[term.value], + this.cbdExtractedMap, + ); + } else { + if (!this.topology.forwards[term.value]) { + const ne: Extracted = { + forwards: {}, + backwards: {}, + }; + ne.backwards[term.value] = this.topology; + this.topology.forwards[term.value] = ne; + } + return new CbdExtracted( + this.topology.forwards[term.value], + this.cbdExtractedMap, + ); + } + } + + enter(term: Term, inverse: boolean): CbdExtracted | undefined { + const out = inverse + ? this.topology.backwards[term.value] + : this.topology.forwards[term.value]; + if (out) { + return new CbdExtracted(out, this.cbdExtractedMap); + } + } +} + +class ExtractInstance { + dereferenced: Set = new Set(); + store: RdfStore; + + dereferencer: RdfDereferencer; + options: CBDShapeExtractorOptions; + graphsToIgnore: string[]; + + shapesGraph?: ShapesGraph; + + constructor( + store: RdfStore, + dereferencer: RdfDereferencer, + graphsToIgnore: string[], + options: CBDShapeExtractorOptions, + shapesGraph?: ShapesGraph, + ) { + this.store = store; + this.dereferencer = dereferencer; + this.shapesGraph = shapesGraph; + this.graphsToIgnore = graphsToIgnore; + this.options = options; + } + + private async dereference(url: string): Promise { + if (this.dereferenced.has(url)) { + log("Will not dereference " + url + " again"); + + return false; + } + this.dereferenced.add(url); + + await this.loadQuadStreamInStore( + ( + await this.dereferencer.dereference(url, { + fetch: this.options.fetch, + }) + ).data, + ); + return true; + } + + public async extract( + id: Term, + offline: boolean, + shapeId?: Term | ShapeTemplate, + ) { + const result = await this.maybeExtractRecursively( + id, + new CbdExtracted(), + offline, + shapeId, + ); + + result.push(...this.store.getQuads(null, null, null, id)); + + if (result.length === 0) { + if (await this.dereference(id.value)) { + // retry + const result = await this.maybeExtractRecursively( id, - cbdExtracted, - dontExtractFromGraph, - result, - false, + new CbdExtracted(), + offline, shapeId, ); - // also add the quads where the named graph matches the current id - result.push(...store.getQuads(null, null, null, id)); - - if (result.length === 0) { - return await dereferenceAndRetry(id.value, "no quads found at all"); - } - } catch (ex) { - if (ex instanceof DereferenceNeeded) { - return await dereferenceAndRetry(ex.target, ex.msg); - } - throw ex; + return result.filter((value: Quad, index: number, array: Quad[]) => { + return index === array.findIndex((x) => x.equals(value)); + }); } - return result; - }; - - const result = await tryExtract(); + } - // When returning the quad array, remove duplicate triples as CBD, required properties, etc. could have added multiple times the same triple return result.filter((value: Quad, index: number, array: Quad[]) => { return index === array.findIndex((x) => x.equals(value)); }); } - /** - * Fills the extraPaths and extraNodeLinks parameters with the ones from valid items in the atLeastOneLists - */ - private recursivelyProcessAtLeastOneLists( + private async maybeExtractRecursively( + id: Term, extracted: CbdExtracted, - shape: ShapeTemplate, - extraPaths: Array, - extraNodeLinks: Array, - ) { - for (let list of shape.atLeastOneLists) { - for (let item of list) { - extraPaths.push(...item.requiredPaths); - extraPaths.push(...item.optionalPaths); - extraNodeLinks.push(...item.nodeLinks); - this.recursivelyProcessAtLeastOneLists( - extracted, - item, - extraPaths, - extraNodeLinks, - ); - } + offline: boolean, + shapeId?: Term | ShapeTemplate, + ): Promise> { + if (extracted.cbdExtracted(id)) { + return []; } + extracted.addShapeTerm(id); + return this.extractRecursively(id, extracted, offline, shapeId); } private async extractRecursively( - store: RdfStore, id: Term, extracted: CbdExtracted, - graphsToIgnore: Array, - result: Quad[], offline: boolean, shapeId?: Term | ShapeTemplate, - ): Promise { - // If it has already been extracted, don’t extract it again: prevents cycles - if (extracted.cbdExtracted(id)) { - return; - } - extracted.addShapeTerm(id); + ): Promise> { + const result: Quad[] = []; let shape: ShapeTemplate | undefined; if (shapeId instanceof ShapeTemplate) { @@ -243,31 +338,19 @@ export class CBDShapeExtractor { shape = this.shapesGraph.shapes.get(shapeId); } - // Perform CBD and we’re done, except on the condition there’s a shape defined and it’s closed - if (!(shape && shape.closed)) { - this.CBD(result, extracted, store, id, graphsToIgnore); + if (!shape?.closed) { + this.CBD(id, result, extracted, this.graphsToIgnore); } - // First, let’s check whether we need to do an HTTP request: - // -- first, this id:Term needs to be an instanceof NamedNode - because otherwise we have nothing to dereference - // --- Next, we can check the required paths - // ----If all paths are set, only then we should also check the atLeastOneLists and check whether it contains a list where none items have set their required properties. - // Next, on our newly fetched data, // we’ll need to process all paths of the shape. If the shape is open, we’re going to do CBD afterwards, so let’s omit paths with only a PredicatePath when the shape is open if (!!shape) { - let visited: Quad[] = []; //For all valid items in the atLeastOneLists, process the required path, optional paths and nodelinks. Do the same for the atLeastOneLists inside these options. let extraPaths: Path[] = []; let extraNodeLinks: NodeLink[] = []; // Process atLeastOneLists in extraPaths and extra NodeLinks - this.recursivelyProcessAtLeastOneLists( - extracted, - shape, - extraPaths, - extraNodeLinks, - ); + shape.fillPathsAndLinks(extraPaths, extraNodeLinks); for (let path of shape.requiredPaths.concat( shape.optionalPaths, @@ -275,117 +358,76 @@ export class CBDShapeExtractor { )) { if (!path.found(extracted) || shape.closed) { let pathQuads = path - .match(store, extracted, id, graphsToIgnore) - .map((pathResult: PathResult) => { - // if the shape is open and thus CBD is going to take place, - // and the subject of that first item is the focusnode (otherwise the first element was a reverse path) - // remove the first element from the quads list of the matches, - if ( - !shape!.closed && - pathResult.path[0].subject.value === id.value - ) { - pathResult.path.shift(); - } + .match(this.store, extracted, id, this.graphsToIgnore) + .flatMap((pathResult) => { return pathResult.path; - }) - .flat() - .filter((quad) => { - // Make sure we don’t add quads multiple times - if (!visited.find((x) => x.equals(quad))) { - visited.push(quad); - return true; - } - - return false; }); - result.push(...pathQuads); // concat all quad paths in the results + result.push(...pathQuads); } } for (let nodeLink of shape.nodeLinks.concat(extraNodeLinks)) { let matches = nodeLink.pathPattern.match( - store, + this.store, extracted, id, - graphsToIgnore, + this.graphsToIgnore, ); // I don't know how to do this correctly, but this is not the way for (let match of matches) { - await this.extractRecursively( - store, - match.target, - match.cbdExtracted, - graphsToIgnore, - result, - offline, - nodeLink.link, + result.push( + ...(await this.maybeExtractRecursively( + match.target, + match.cbdExtracted, + offline, + nodeLink.link, + )), ); } - - let pathQuads = Array.from( - nodeLink.pathPattern.match(store, extracted, id, graphsToIgnore), - ) - .map((pathResult: PathResult) => { - // if the shape is open and thus CBD is going to take place - // and if the subject of that first item is the focusnode (otherwise the first element was a reverse path) - // remove the first element from the quads list of the matches, - if ( - !shape?.closed && - pathResult.path[0].subject.value === id.value - ) { - pathResult.path.shift(); - } - return pathResult.path; - }) - .flat() - .filter((quad) => { - // Make sure we don’t add quads multiple times - // There must be a more efficient solution to making sure there’s only one of each triple... - if (!visited.find((x) => x.equals(quad))) { - visited.push(quad); - return true; - } - return false; - }); - - result.push(...pathQuads); //concat all quad paths in the results } } - if (!offline && id.termType === "NamedNode" && shape) { - // Check required paths and lazy evaluate the atLeastOneLists - const problems = shape.requiredAreNotPresent(extracted); - if (problems) { - throw new DereferenceNeeded( - id.value, - `not all paths are found (${problems.toString()})`, - ); + if (!offline && id.termType === "NamedNode") { + if (shape) { + const problems = shape.requiredAreNotPresent(extracted); + if (problems) { + if (await this.dereference(id.value)) { + // retry + return this.extractRecursively(id, extracted, offline, shapeId); + } else { + log( + `${ + id.value + } does not adhere to the shape (${problems.toString()})`, + ); + } + } } } + + return result; } /** * Performs Concise Bounded Description: extract star-shape and recurses over the blank nodes * @param result list of quads - * @param extractedStar topology object to keep track of already found properties + * @param extractedStar topology object to keep track of already found properties * @param store store to use for cbd * @param id starting subject - * @param graphsToIgnore + * @param graphsToIgnore */ - public async CBD( + private async CBD( + id: Term, result: Quad[], extractedStar: CbdExtracted, - store: RdfStore, - id: Term, graphsToIgnore: Array, ) { extractedStar.addCBDTerm(id); - const graph = this.options.cbdDefaultGraph ? df.defaultGraph(): null; - const quads = store.getQuads(id, null, null, graph); + const graph = this.options.cbdDefaultGraph ? df.defaultGraph() : null; + const quads = this.store.getQuads(id, null, null, graph); - // Iterate over the quads, add them to the result and check whether we should further get other quads based on blank nodes or the SHACL shape for (const q of quads) { // Ignore quads in the graphs to ignore if (graphsToIgnore?.includes(q.graph.value)) { @@ -397,106 +439,17 @@ export class CBDShapeExtractor { // Conditionally get more quads: if it’s a not yet extracted blank node if ( - q.object.termType === 'BlankNode' && + q.object.termType === "BlankNode" && !extractedStar.cbdExtracted(q.object) ) { - // Only perform CBD again recursively on the blank node - await this.CBD(result, next, store, q.object, graphsToIgnore); - } - } - - // Should we also take into account RDF* and/or RDF reification systems here? - } -} - -export type Extracted = { - forwards: { - [node: string]: Extracted; - }; - backwards: { - [node: string]: Extracted; - }; -}; - -export type ExtractReasons = { - cbd: boolean; - shape: boolean; -}; - -export class CbdExtracted { - topology: Extracted; - cbdExtractedMap: RDFMap; - - constructor( - topology?: Extracted, - cbdExtracted: RDFMap = new RDFMap(), - ) { - if (topology) { - this.topology = topology; - } else { - this.topology = { forwards: {}, backwards: {} }; - } - this.cbdExtractedMap = cbdExtracted; - } - - addCBDTerm(term: Term) { - const t = this.cbdExtractedMap.get(term); - if (t) { - t.cbd = true; - } else { - this.cbdExtractedMap.set(term, { cbd: true, shape: false }); - } - } - - addShapeTerm(term: Term) { - const t = this.cbdExtractedMap.get(term); - if (t) { - t.shape = true; - } else { - this.cbdExtractedMap.set(term, { cbd: true, shape: false }); - } - } - - cbdExtracted(term: Term): boolean { - return !!this.cbdExtractedMap.get(term)?.shape; - } - - push(term: Term, inverse: boolean): CbdExtracted { - if (inverse) { - if (!this.topology.backwards[term.value]) { - const ne: Extracted = { - forwards: {}, - backwards: {}, - }; - ne.forwards[term.value] = this.topology; - this.topology.backwards[term.value] = ne; + await this.CBD(q.object, result, next, graphsToIgnore); } - return new CbdExtracted( - this.topology.backwards[term.value], - this.cbdExtractedMap, - ); - } else { - if (!this.topology.forwards[term.value]) { - const ne: Extracted = { - forwards: {}, - backwards: {}, - }; - ne.backwards[term.value] = this.topology; - this.topology.forwards[term.value] = ne; - } - return new CbdExtracted( - this.topology.forwards[term.value], - this.cbdExtractedMap, - ); } } - enter(term: Term, inverse: boolean): CbdExtracted | undefined { - const out = inverse - ? this.topology.backwards[term.value] - : this.topology.forwards[term.value]; - if (out) { - return new CbdExtracted(out, this.cbdExtractedMap); - } + private loadQuadStreamInStore(quadStream: any) { + return new Promise((resolve, reject) => { + this.store.import(quadStream).on("end", resolve).on("error", reject); + }); } } diff --git a/lib/Shape.ts b/lib/Shape.ts index 0a247a4..392deb8 100644 --- a/lib/Shape.ts +++ b/lib/Shape.ts @@ -1,6 +1,6 @@ import { RdfStore } from "rdf-stores"; import { Term } from "@rdfjs/types"; -import { DataFactory } from 'rdf-data-factory'; +import { DataFactory } from "rdf-data-factory"; const df = new DataFactory(); import { createTermNamespace, RDF } from "@treecg/types"; import { @@ -34,18 +34,27 @@ const SHACL = createTermNamespace( "NodeShape", ); - -const getSubjects = function (store: RdfStore, predicate: Term|null, object: Term|null, graph?:Term|null) { +const getSubjects = function ( + store: RdfStore, + predicate: Term | null, + object: Term | null, + graph?: Term | null, +) { return store.getQuads(null, predicate, object, graph).map((quad) => { return quad.subject; }); -} - -const getObjects = function (store: RdfStore, subject:Term|null, predicate: Term|null, graph?:Term|null) { +}; + +const getObjects = function ( + store: RdfStore, + subject: Term | null, + predicate: Term | null, + graph?: Term | null, +) { return store.getQuads(subject, predicate, null, graph).map((quad) => { return quad.object; }); -} +}; //TODO: split this file up between Shape functionality and SHACL to our Shape class conversion steps. Also introduce a ShEx to Shape Template export class NodeLink { @@ -92,6 +101,18 @@ export class ShapeTemplate { this.closed = false; //default value } + fillPathsAndLinks(extraPaths: Array, extraNodeLinks: Array) { + for (let list of this.atLeastOneLists) { + for (let item of list) { + extraPaths.push(...item.requiredPaths); + extraPaths.push(...item.optionalPaths); + // extraPaths.push(...item.nodeLinks.map((x) => x.pathPattern)); + extraNodeLinks.push(...item.nodeLinks); + item.fillPathsAndLinks(extraPaths, extraNodeLinks); + } + } + } + private invalidAtLeastOneLists( extract: CbdExtracted, ): ShapeError | undefined { @@ -177,28 +198,34 @@ export class ShapesGraph { } protected constructPathPattern(shapeStore: RdfStore, listItem: Term): Path { - if (listItem.termType === 'BlankNode') { + if (listItem.termType === "BlankNode") { //Look for special types - let zeroOrMorePathObjects = getObjects(shapeStore, + let zeroOrMorePathObjects = getObjects( + shapeStore, listItem, SHACL.zeroOrMorePath, null, ); - let oneOrMorePathObjects = getObjects(shapeStore, + let oneOrMorePathObjects = getObjects( + shapeStore, listItem, SHACL.oneOrMorePath, null, ); - let zeroOrOnePathObjects = getObjects(shapeStore, + let zeroOrOnePathObjects = getObjects( + shapeStore, listItem, SHACL.zeroOrOnePath, null, ); - let inversePathObjects = getObjects(shapeStore, + let inversePathObjects = getObjects( + shapeStore, listItem, - SHACL.inversePath, null, + SHACL.inversePath, + null, ); - let alternativePathObjects = getObjects(shapeStore, + let alternativePathObjects = getObjects( + shapeStore, listItem, SHACL.alternativePath, null, @@ -251,7 +278,8 @@ export class ShapesGraph { required?: boolean, ): boolean { //Skip if shape has been deactivated - let deactivated = getObjects(shapeStore, + let deactivated = getObjects( + shapeStore, propertyShapeId, SHACL.deactivated, null, @@ -260,7 +288,7 @@ export class ShapesGraph { return true; //Success: doesn’t matter what kind of thing it was, it’s deactivated so let’s just proceed } - let path = getObjects(shapeStore,propertyShapeId, SHACL.path, null)[0]; + let path = getObjects(shapeStore, propertyShapeId, SHACL.path, null)[0]; //Process the path now and make sure there’s a match function if (!path) { return false; //this isn’t a property shape... @@ -268,7 +296,12 @@ export class ShapesGraph { let pathPattern = this.constructPathPattern(shapeStore, path); - let minCount = getObjects(shapeStore,propertyShapeId, SHACL.minCount, null); + let minCount = getObjects( + shapeStore, + propertyShapeId, + SHACL.minCount, + null, + ); if ((minCount[0] && minCount[0].value !== "0") || required) { shape.requiredPaths.push(pathPattern); @@ -280,7 +313,7 @@ export class ShapesGraph { // Maybe to potentially point to another node, xone a datatype? // Does it link to a literal or to a new node? - let nodeLink = getObjects(shapeStore,propertyShapeId, SHACL.node, null); + let nodeLink = getObjects(shapeStore, propertyShapeId, SHACL.node, null); if (nodeLink[0]) { shape.nodeLinks.push(new NodeLink(pathPattern, nodeLink[0])); } @@ -313,7 +346,8 @@ export class ShapesGraph { shape: ShapeTemplate, ) { //Check if it’s closed or open - let closedIndicator: Term = getObjects(shapeStore, + let closedIndicator: Term = getObjects( + shapeStore, nodeShapeId, SHACL.closed, null, @@ -323,14 +357,14 @@ export class ShapesGraph { } //Process properties if it has any - let properties = getObjects(shapeStore,nodeShapeId, SHACL.property, null); + let properties = getObjects(shapeStore, nodeShapeId, SHACL.property, null); for (let prop of properties) { this.preprocessPropertyShape(shapeStore, prop, shape); } // process sh:and: just add all IDs to this array // Process everything you can find nested in AND clauses - for (let andList of getObjects(shapeStore,nodeShapeId, SHACL.and, null)) { + for (let andList of getObjects(shapeStore, nodeShapeId, SHACL.and, null)) { // Try to process it as a property shape //for every andList found, iterate through it and try to preprocess the property shape for (let and of this.rdfListToArray(shapeStore, andList)) { @@ -338,8 +372,12 @@ export class ShapesGraph { } } //Process zero or more sh:xone and sh:or lists in the same way -- explanation in README why they can be handled in the same way - for (let xoneOrOrList of getObjects(shapeStore, nodeShapeId, SHACL.xone, null) - .concat(getObjects(shapeStore,nodeShapeId, SHACL.or, null))) { + for (let xoneOrOrList of getObjects( + shapeStore, + nodeShapeId, + SHACL.xone, + null, + ).concat(getObjects(shapeStore, nodeShapeId, SHACL.or, null))) { let atLeastOneList: Array = this.rdfListToArray( shapeStore, xoneOrOrList, @@ -364,7 +402,7 @@ export class ShapesGraph { const shapeNodes: Term[] = ([]) .concat(getSubjects(shapeStore, SHACL.property, null, null)) .concat(getSubjects(shapeStore, RDF.terms.type, SHACL.NodeShape, null)) - .concat(getObjects(shapeStore,null, SHACL.node, null)) + .concat(getObjects(shapeStore, null, SHACL.node, null)) //DISTINCT .filter((value: Term, index: number, array: Array) => { return array.findIndex((x) => x.equals(value)) === index; @@ -374,7 +412,12 @@ export class ShapesGraph { for (let shapeId of shapeNodes) { let shape = new ShapeTemplate(); //Don’t process if shape is deactivated - let deactivated = getObjects(shapeStore,shapeId, SHACL.deactivated, null); + let deactivated = getObjects( + shapeStore, + shapeId, + SHACL.deactivated, + null, + ); if (!(deactivated.length > 0 && deactivated[0].value === "true")) { this.preprocessNodeShape(shapeStore, shapeId, shape); shapes.set(shapeId, shape); @@ -394,18 +437,21 @@ export class ShapesGraph { item: Term, ): Generator { if ( - getObjects(shapeStore, + getObjects( + shapeStore, item, df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), null, )[0] ) { - yield getObjects(shapeStore, + yield getObjects( + shapeStore, item, df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), null, )[0]; - let rest = getObjects(shapeStore, + let rest = getObjects( + shapeStore, item, df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), null, @@ -414,12 +460,14 @@ export class ShapesGraph { rest && rest.value !== "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil" ) { - yield getObjects(shapeStore, + yield getObjects( + shapeStore, rest, df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), null, )[0]; - rest = getObjects(shapeStore, + rest = getObjects( + shapeStore, rest, df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), null, diff --git a/tests/01 - fetching a shacl shape/extraction.test.ts b/tests/01 - fetching a shacl shape/extraction.test.ts index 238dac3..90a4f46 100644 --- a/tests/01 - fetching a shacl shape/extraction.test.ts +++ b/tests/01 - fetching a shacl shape/extraction.test.ts @@ -74,6 +74,6 @@ describe("Check whether we can successfully extract a SHACL shape", async () => writer.end((err, res) => {console.log(res);});*/ //TODO: Didn’t yet calculate how many actually should be returned here... Just assumed this number is correct... - assert.equal(result.length, 273); // Just testing whether there are quads being returned now + assert.equal(result.length, 264); // Just testing whether there are quads being returned now }); }); diff --git a/tests/04 - logical edge cases/testExtraction.test.ts b/tests/04 - logical edge cases/testExtraction.test.ts index 8632670..850f044 100644 --- a/tests/04 - logical edge cases/testExtraction.test.ts +++ b/tests/04 - logical edge cases/testExtraction.test.ts @@ -98,3 +98,61 @@ describe("Extracting logical edge cases", function () { assert.equal(result.length, 7); }); }); + +describe("Check whether paths are correctly chained", async () => { + const shape = ` +@prefix sh: . +@prefix ex: . + +ex:innerShape + a sh:NodeShape ; + sh:property [ + sh:path (ex:first ex:second); + ] . + +ex:outerShape + a sh:NodeShape ; + sh:property [ + sh:path ex:inner; + sh:node ex:innerShape; + ] . +`; + + const data = ` +@prefix ex: . + +ex:false ex:second "Don't find me". + +ex:true ex:first ex:trueInner. +ex:trueInner ex:second "Find me". + +ex:subject ex:first ex:false; + ex:inner ex:true. +`; + + let shapeStore = RdfStore.createDefault(); + let extractor: CBDShapeExtractor; + let dataStore = RdfStore.createDefault(); + + const shapeQuads = new Parser().parse(shape); + const dataQuads = new Parser().parse(data); + + shapeQuads.forEach((quad) => shapeStore.addQuad(quad)); + dataQuads.forEach((quad) => dataStore.addQuad(quad)); + extractor = new CBDShapeExtractor(shapeStore); + + const entity = await extractor.extract( + dataStore, + new NamedNode("http://example.org/subject"), + new NamedNode("http://example.org/outerShape"), + ); + it("Follows complex path inside inner node", async () => { + const findMe = entity.find((q) => q.object.value === "Find me"); + assert.isTrue(!!findMe, "Find me is found"); + }); + + it("Doesn't follow path if it is not part of the shape", async () => { + const findMe = entity.find((q) => q.object.value === "Don't find me"); + assert.isTrue(!findMe, "Don't find me is not found"); + }); +});