From 66e397820bef10d21173d824f3b34a5ec5695b87 Mon Sep 17 00:00:00 2001 From: Pieter Heyvaert Date: Mon, 6 May 2024 10:47:16 +0200 Subject: [PATCH] Shape to mermaid (#30) * Add a shapeGraph to mermaid functionality * optional paths, nodeLinks and atLeastOneLists * inverse path * Add error test + docs * Extract label for Node Shapes from rdfs:label or IRI * Added a simple binary for mermaid * bump version --------- Co-authored-by: Julian Rojas Co-authored-by: Pieter Colpaert Co-authored-by: ajuvercr --- bin/mermaid.ts | 85 +++ lib/CBDShapeExtractor.ts | 3 +- lib/Shape.ts | 357 +----------- lib/ShapesGraph.ts | 540 ++++++++++++++++++ package-lock.json | 25 +- package.json | 5 +- .../shapeTemplate.test.ts | 2 +- .../testShapeTemplate.test.ts | 3 +- tests/05 - paths/pathPattern.test.ts | 2 +- tests/05 - paths/shapeTemplate.test.ts | 3 +- tests/07 - mermaid/all-together-path.txt | 3 + tests/07 - mermaid/alternative-path.txt | 3 + tests/07 - mermaid/double-inverse-path.txt | 3 + tests/07 - mermaid/inverse-path.txt | 3 + tests/07 - mermaid/mermaid.test.ts | 144 +++++ tests/07 - mermaid/nested-shape.txt | 5 + .../nested-with-optional-shape.txt | 5 + tests/07 - mermaid/one-or-more-path.txt | 3 + tests/07 - mermaid/optional-inverse-path.txt | 3 + tests/07 - mermaid/optional-sequence-path.txt | 3 + tests/07 - mermaid/quadruple-inverse-path.txt | 3 + .../sequence-and-inverse-path.txt | 3 + tests/07 - mermaid/sequence-path.txt | 3 + tests/07 - mermaid/shape.ttl | 166 ++++++ tests/07 - mermaid/triple-inverse-path.txt | 3 + tests/07 - mermaid/xone-with-node-shape-2.txt | 12 + tests/07 - mermaid/xone-with-node-shape.txt | 10 + tests/07 - mermaid/zero-or-more-path.txt | 3 + tests/07 - mermaid/zero-or-one-path.txt | 3 + 29 files changed, 1046 insertions(+), 360 deletions(-) create mode 100644 bin/mermaid.ts create mode 100644 lib/ShapesGraph.ts create mode 100644 tests/07 - mermaid/all-together-path.txt create mode 100644 tests/07 - mermaid/alternative-path.txt create mode 100644 tests/07 - mermaid/double-inverse-path.txt create mode 100644 tests/07 - mermaid/inverse-path.txt create mode 100644 tests/07 - mermaid/mermaid.test.ts create mode 100644 tests/07 - mermaid/nested-shape.txt create mode 100644 tests/07 - mermaid/nested-with-optional-shape.txt create mode 100644 tests/07 - mermaid/one-or-more-path.txt create mode 100644 tests/07 - mermaid/optional-inverse-path.txt create mode 100644 tests/07 - mermaid/optional-sequence-path.txt create mode 100644 tests/07 - mermaid/quadruple-inverse-path.txt create mode 100644 tests/07 - mermaid/sequence-and-inverse-path.txt create mode 100644 tests/07 - mermaid/sequence-path.txt create mode 100644 tests/07 - mermaid/shape.ttl create mode 100644 tests/07 - mermaid/triple-inverse-path.txt create mode 100644 tests/07 - mermaid/xone-with-node-shape-2.txt create mode 100644 tests/07 - mermaid/xone-with-node-shape.txt create mode 100644 tests/07 - mermaid/zero-or-more-path.txt create mode 100644 tests/07 - mermaid/zero-or-one-path.txt diff --git a/bin/mermaid.ts b/bin/mermaid.ts new file mode 100644 index 0000000..76a4c80 --- /dev/null +++ b/bin/mermaid.ts @@ -0,0 +1,85 @@ +import { assert } from "chai"; +import { RdfStore } from "rdf-stores"; +import { ShapesGraph } from "../lib/ShapesGraph"; +import { DataFactory } from "rdf-data-factory"; +import rdfDereference from "rdf-dereference"; +import fs from "fs/promises"; +import * as process from 'process'; +import { Term } from "rdf-js"; +import { deflate} from "pako"; +import { fromUint8Array } from 'js-base64'; + +const df = new DataFactory(); + + +// Check if at least one command line argument is provided +if (process.argv.length <= 2) { + console.error('Please provide an IRI to a dereferenceable SHACL NodeShape or an LDES or tree:Collection with with tree:shape property in it'); + process.exit(1); // Exit with an error code +} + +let iri = process.argv[2]; + +async function main () { + let df = new DataFactory(); + let shapeStore = RdfStore.createDefault(); + let shapesGraph: ShapesGraph; + let shapeTerm: Term = df.namedNode(iri); + let readStream = ( + await rdfDereference.dereference(iri, { + localFiles: true, + }) + ).data; + + await new Promise((resolve, reject) => { + shapeStore.import(readStream).on("end", resolve).on("error", reject); + }); + let tmpShapeTerm: Term[] = shapeStore.getQuads(null, df.namedNode('https://w3id.org/tree#shape'), null).map((quad) => quad.object); + if (tmpShapeTerm[0]) { + shapeTerm = tmpShapeTerm[0]; + iri = shapeTerm.value; + } + if (tmpShapeTerm[0] && tmpShapeTerm[0].termType==='NamedNode') { + //Dereference the shape and add it here. The iri is not this IRI + console.error('GET ' + shapeTerm.value); + //Try to dereference this one as well. If it works, nice, if it doesn’t, too bad, we’ll continue without notice. + let readStream2 = ( + await rdfDereference.dereference(shapeTerm.value, { + localFiles: true, + }) + ).data; + await new Promise((resolve, reject) => { + shapeStore.import(readStream2).on("end", resolve).on("error", () => { + console.error('Warning: couldn’t fetch ' + iri + ' but continuing'); + resolve(null); + }); + }); + } + + shapesGraph = new ShapesGraph(shapeStore); + const actualMermaid = shapesGraph.toMermaid(shapeTerm); + console.log('```mermaid'); + console.log(actualMermaid); + console.log('```'); + + + const formatJSON = (data: unknown): string => JSON.stringify(data, undefined, 2); + const serialize = (state: string): string => { + const data = new TextEncoder().encode(state); + const compressed = deflate(data, { level: 9 }); // zlib level 9 + return fromUint8Array(compressed, true); // url safe base64 encoding + } + const defaultState = { + code: actualMermaid, + mermaid: formatJSON({ + theme: 'default' + }), + autoSync: true, + updateDiagram: true + }; + const json = JSON.stringify(defaultState); + const serialized = serialize(json); + console.log(); + console.log('Mermaid Live: https://mermaid.live/edit#pako:'+ serialized); +} +main(); \ No newline at end of file diff --git a/lib/CBDShapeExtractor.ts b/lib/CBDShapeExtractor.ts index 112af81..2eb4296 100644 --- a/lib/CBDShapeExtractor.ts +++ b/lib/CBDShapeExtractor.ts @@ -1,10 +1,11 @@ import rdfDereference, { RdfDereferencer } from "rdf-dereference"; -import { NodeLink, RDFMap, ShapesGraph, ShapeTemplate } from "./Shape"; +import { NodeLink, RDFMap, ShapeTemplate } from "./Shape"; import { Path, PathResult } from "./Path"; import { DataFactory } from "rdf-data-factory"; import { RdfStore } from "rdf-stores"; import { Quad, Term } from "@rdfjs/types"; import debug from "debug"; +import {ShapesGraph} from "./ShapesGraph"; const log = debug("extract-cbd-shape"); diff --git a/lib/Shape.ts b/lib/Shape.ts index 392deb8..d313798 100644 --- a/lib/Shape.ts +++ b/lib/Shape.ts @@ -1,60 +1,6 @@ -import { RdfStore } from "rdf-stores"; -import { Term } from "@rdfjs/types"; -import { DataFactory } from "rdf-data-factory"; -const df = new DataFactory(); -import { createTermNamespace, RDF } from "@treecg/types"; -import { - AlternativePath, - InversePath, - OneOrMorePath, - Path, - PredicatePath, - SequencePath, - ZeroOrMorePath, - ZeroOrOnePath, -} from "./Path"; -import { CbdExtracted } from "./CBDShapeExtractor"; - -const SHACL = createTermNamespace( - "http://www.w3.org/ns/shacl#", - "zeroOrMorePath", - "zeroOrOnePath", - "oneOrMorePath", - "inversePath", - "alternativePath", - "deactivated", - "minCount", - "path", - "node", - "closed", - "property", - "and", - "xone", - "or", - "NodeShape", -); - -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, -) { - return store.getQuads(subject, predicate, null, graph).map((quad) => { - return quad.object; - }); -}; +import {Term} from "@rdfjs/types"; +import {Path,} from "./Path"; +import {CbdExtracted} from "./CBDShapeExtractor"; //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 { @@ -90,11 +36,12 @@ export class ShapeTemplate { requiredPaths: Array; optionalPaths: Array; atLeastOneLists: Array>; + label?: string; constructor() { //All properties will be added, but if a required property is not available, then we need to further look it up this.requiredPaths = []; - //If there’s a nodelink through one of the properties, I want to know what other shape to look up in the shapesgraph from there + //If there’s a nodelink through one of the properties, I want to know what other shape to look up in the shapes graph from there this.nodeLinks = []; this.atLeastOneLists = []; this.optionalPaths = []; @@ -190,297 +137,3 @@ export class RDFMap { } } -export class ShapesGraph { - shapes: RDFMap; - - constructor(shapeStore: RdfStore) { - this.shapes = this.initializeFromStore(shapeStore); - } - - protected constructPathPattern(shapeStore: RdfStore, listItem: Term): Path { - if (listItem.termType === "BlankNode") { - //Look for special types - let zeroOrMorePathObjects = getObjects( - shapeStore, - listItem, - SHACL.zeroOrMorePath, - null, - ); - let oneOrMorePathObjects = getObjects( - shapeStore, - listItem, - SHACL.oneOrMorePath, - null, - ); - let zeroOrOnePathObjects = getObjects( - shapeStore, - listItem, - SHACL.zeroOrOnePath, - null, - ); - let inversePathObjects = getObjects( - shapeStore, - listItem, - SHACL.inversePath, - null, - ); - let alternativePathObjects = getObjects( - shapeStore, - listItem, - SHACL.alternativePath, - null, - ); - if (zeroOrMorePathObjects[0]) { - return new ZeroOrMorePath( - this.constructPathPattern(shapeStore, zeroOrMorePathObjects[0]), - ); - } else if (oneOrMorePathObjects[0]) { - return new OneOrMorePath( - this.constructPathPattern(shapeStore, oneOrMorePathObjects[0]), - ); - } else if (zeroOrOnePathObjects[0]) { - return new ZeroOrOnePath( - this.constructPathPattern(shapeStore, zeroOrOnePathObjects[0]), - ); - } else if (inversePathObjects[0]) { - return new InversePath( - this.constructPathPattern(shapeStore, inversePathObjects[0]), - ); - } else if (alternativePathObjects[0]) { - let alternativeListArray = this.rdfListToArray( - shapeStore, - alternativePathObjects[0], - ).map((value: Term) => { - return this.constructPathPattern(shapeStore, value); - }); - return new AlternativePath(alternativeListArray); - } else { - const items = this.rdfListToArray(shapeStore, listItem); - return new SequencePath( - items.map((x) => this.constructPathPattern(shapeStore, x)), - ); - } - } - - return new PredicatePath(listItem); - } - - /** - * @param shapeStore - * @param propertyShapeId - * @param shape - * @returns false if it wasn’t a property shape - */ - protected preprocessPropertyShape( - shapeStore: RdfStore, - propertyShapeId: Term, - shape: ShapeTemplate, - required?: boolean, - ): boolean { - //Skip if shape has been deactivated - let deactivated = getObjects( - shapeStore, - propertyShapeId, - SHACL.deactivated, - null, - ); - if (deactivated.length > 0 && deactivated[0].value === "true") { - 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]; - //Process the path now and make sure there’s a match function - if (!path) { - return false; //this isn’t a property shape... - } - - let pathPattern = this.constructPathPattern(shapeStore, path); - - let minCount = getObjects( - shapeStore, - propertyShapeId, - SHACL.minCount, - null, - ); - - if ((minCount[0] && minCount[0].value !== "0") || required) { - shape.requiredPaths.push(pathPattern); - } else { - //TODO: don’t include node links? - shape.optionalPaths.push(pathPattern); - } - // **TODO**: will the sh:or, sh:xone, sh:and, etc. be of use here? It won’t contain any more information about possible properties? - // 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); - if (nodeLink[0]) { - shape.nodeLinks.push(new NodeLink(pathPattern, nodeLink[0])); - } - //TODO: Can Nodelinks appear in conditionals from here? Probably they can? (same comment as ↑) - return true; // Success: the property shape has been processed - } - - /** - * Processes a NodeShape or PropertyShape and adds NodeLinks and required properties to the arrays. - * @param shapeStore - * @param shapeId - * @param shape - * @returns - */ - preprocessShape(shapeStore: RdfStore, shapeId: Term, shape: ShapeTemplate) { - return this.preprocessPropertyShape(shapeStore, shapeId, shape) - ? true - : this.preprocessNodeShape(shapeStore, shapeId, shape); - } - - /** - * Processes a NodeShape - * @param shapeStore - * @param nodeShapeId - * @param shape - */ - protected preprocessNodeShape( - shapeStore: RdfStore, - nodeShapeId: Term, - shape: ShapeTemplate, - ) { - //Check if it’s closed or open - let closedIndicator: Term = getObjects( - shapeStore, - nodeShapeId, - SHACL.closed, - null, - )[0]; - if (closedIndicator && closedIndicator.value === "true") { - shape.closed = true; - } - - //Process properties if it has any - 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)) { - // 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)) { - this.preprocessShape(shapeStore, and, shape); - } - } - //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))) { - let atLeastOneList: Array = this.rdfListToArray( - shapeStore, - xoneOrOrList, - ).map((val): ShapeTemplate => { - let newShape = new ShapeTemplate(); - //Create a new shape and process as usual -- but mind that we don’t trigger a circular shape here... - this.preprocessShape(shapeStore, val, newShape); - return newShape; - //Add this one to the shapesgraph - }); - shape.atLeastOneLists.push(atLeastOneList); - } - //And finally, we’re just ignoring sh:not. Don’t process this one - } - - /** - * @param nodeShape is an N3.Store with the quads of the SHACL shape - */ - initializeFromStore(shapeStore: RdfStore): RDFMap { - //get all named nodes of entities that are sh:ShapeNodes which we’ll recognize through their use of sh:property (we’ll find other relevant shape nodes later on) - //TODO: This is a limitation though: we only support NodeShapes with at least one sh:property set? Other NodeShapes in this context are otherwise just meaningless? - 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)) - //DISTINCT - .filter((value: Term, index: number, array: Array) => { - return array.findIndex((x) => x.equals(value)) === index; - }); - - let shapes = new RDFMap(); - for (let shapeId of shapeNodes) { - let shape = new ShapeTemplate(); - //Don’t process if shape is deactivated - 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); - } - } - return shapes; - } - - /** - * Processes all element from an RDF List, or detects it wasn’t a list after all and it’s just one element. - * @param shapeStore - * @param item - * @returns - */ - protected *rdfListToGenerator( - shapeStore: RdfStore, - item: Term, - ): Generator { - if ( - getObjects( - shapeStore, - item, - df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), - null, - )[0] - ) { - yield getObjects( - shapeStore, - item, - df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), - null, - )[0]; - let rest = getObjects( - shapeStore, - item, - df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), - null, - )[0]; - while ( - rest && - rest.value !== "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil" - ) { - yield getObjects( - shapeStore, - rest, - df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), - null, - )[0]; - rest = getObjects( - shapeStore, - rest, - df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), - null, - )[0]; - } - } else { - //it’s not a list, it’s just one element - yield item; - } - return; - } - - protected rdfListToArray(shapeStore: RdfStore, item: Term): Array { - return Array.from(this.rdfListToGenerator(shapeStore, item)); - } -} diff --git a/lib/ShapesGraph.ts b/lib/ShapesGraph.ts new file mode 100644 index 0000000..20ef2f1 --- /dev/null +++ b/lib/ShapesGraph.ts @@ -0,0 +1,540 @@ +import { RdfStore } from "rdf-stores"; +import { Term } from "@rdfjs/types"; +import { + AlternativePath, + InversePath, + OneOrMorePath, + Path, + PredicatePath, + SequencePath, + ZeroOrMorePath, + ZeroOrOnePath +} from "./Path"; +import { createTermNamespace, RDF, RDFS } from "@treecg/types"; +import { NodeLink, RDFMap, ShapeTemplate } from "./Shape"; +import { DataFactory } from "rdf-data-factory"; + +const df = new DataFactory(); + +const SHACL = createTermNamespace( + "http://www.w3.org/ns/shacl#", + "zeroOrMorePath", + "zeroOrOnePath", + "oneOrMorePath", + "inversePath", + "alternativePath", + "deactivated", + "minCount", + "path", + "node", + "closed", + "property", + "and", + "xone", + "or", + "NodeShape", +); + +export class ShapesGraph { + shapes: RDFMap; + private counter: number; + + constructor(shapeStore: RdfStore) { + this.shapes = this.initializeFromStore(shapeStore); + this.counter = 0; + } + + /** + * This function returns a Mermaid representation of a shape identified by a given term. + * @param term {Term} - The term of the Shape that is the start of the representation. + */ + public toMermaid(term: Term): string { + const startShape = this.shapes.get(term); + this.counter = 0; + + if (!startShape) { + throw new Error(`No shape found for term "${term.value}"`); + } + + let mermaid = 'flowchart LR\n'; + mermaid += this.toMermaidSingleShape(startShape, '1', startShape.label || 'Shape'); + return mermaid; + } + + /** + * This function returns a Mermaid representation of a given shape. + * @param shape - The shape for which to generate a representation. + * @param id - The ID to identify the shape in the representation. + * @param name - The name used for the shape in the representation. + * @private + */ + private toMermaidSingleShape(shape: ShapeTemplate, id: string, name: string): string { + let mermaid = ` S${id}((${name}))\n`; + let alreadyProcessedPaths: string[] = []; + + shape.nodeLinks.forEach(nodeLink => { + let p = nodeLink.pathPattern.toString(); + const isPathRequired = this.isPathRequired(p, shape.requiredPaths); + alreadyProcessedPaths.push(p); + p = this.cleanPath(p); + const linkedShape = this.shapes.get(nodeLink.link); + + if (!linkedShape) { + throw new Error(`The linked shape "${nodeLink.link}" is not found`); + } + + const linkedShapeId = `${id}_${this.counter}`; + + let link = '-->'; + + if (!isPathRequired) { + link = '-.->'; + } + if (p.startsWith('^')) { + p = p.substring(1); + mermaid += ` S${linkedShapeId}[ ]${link}|"${p}"|S${id}\n`; + } else { + mermaid += ` S${id}${link}|"${p}"|S${linkedShapeId}[ ]\n`; + } + + this.counter++; + + const linkedShapeMermaid = this.toMermaidSingleShape(linkedShape, linkedShapeId, linkedShape.label || 'Shape'); + mermaid += linkedShapeMermaid; + }); + + shape.atLeastOneLists.forEach(list => { + if (list.length > 0) { + const xId = `${id}_${this.counter}`; + mermaid += ` S${id}---X${xId}{OR}\n`; + + list.forEach(shape => { + const shapeId = `${id}_${this.counter}`; + this.counter++; + + mermaid += ` X${xId}---S${shapeId}\n`; + const linkedShapeMermaid = this.toMermaidSingleShape(shape, shapeId, shape.label || 'Shape'); + mermaid += linkedShapeMermaid; + }); + } + }); + + mermaid += this.simplePathToMermaid(shape.requiredPaths, alreadyProcessedPaths, id, '-->'); + mermaid += this.simplePathToMermaid(shape.optionalPaths, alreadyProcessedPaths, id, '-.->'); + + return mermaid; + } + + /** + * This function removes < and > from a path. + * @param path - The path from which to remove the < and >. + * @private + */ + private cleanPath(path: string): string { + path = path.replace(//g, ''); + } + + /** + * This function returns true if the given path is required. + * @param path - The path that needs to be checked. + * @param requiredPaths - An array of all required paths. + * @private + */ + private isPathRequired(path: string, requiredPaths: Path[]): boolean { + for (const requiredPath of requiredPaths) { + if (path === requiredPath.toString()) { + return true; + } + } + + return false; + } + + /** + * This function returns a Mermaid presentation for an array of simple paths. + * This function is intended to be used with shape.requiredPaths and shape.optionalPaths. + * @param paths - An array of paths. + * @param alreadyProcessedPaths - An array of stringified paths that already have been processed. + * @param shapedId - The id of the shape to which these paths belong. + * @param link - The Mermaid link that needs to be used. + * @private + */ + private simplePathToMermaid(paths: Path[], alreadyProcessedPaths: string[], shapedId: string, link: string) { + let mermaid = ''; + + paths.forEach(path => { + let p = path.toString(); + + if (alreadyProcessedPaths.includes(p)) { + return; + } + + alreadyProcessedPaths.push(p); + p = this.cleanPath(p); + + if (this.isRealInversePath(p)) { + p = this.getRealPath(p); + mermaid += ` S${shapedId}_${this.counter}[ ]${link}|"${p}"|S${shapedId}\n`; + } else { + p = this.getRealPath(p); + mermaid += ` S${shapedId}${link}|"${p}"|S${shapedId}_${this.counter}[ ]\n`; + } + + this.counter++; + }); + + return mermaid; + } + + /** + * This function returns true if a given path is real inverse path. + * This means that the path is not a double, quadruple, ... inverse path. + * @param path - The path that needs to be checked. + * @private + */ + private isRealInversePath(path: string): boolean { + const found = path.match(/^(\^+)[^\^]+/); + + if (!found) { + return false; + } + + return found[1].length % 2 !== 0; + } + + /** + * This function removes all the ^ from the path. + * @param path - The path from which to remove the ^. + * @private + */ + private getRealPath(path: string): string { + const found = path.match(/^\^*([^\^]+)/); + + if (!found) { + throw new Error(`No real path found in "${path}"`); + } + + return found[1]; + } + + protected constructPathPattern(shapeStore: RdfStore, listItem: Term): Path { + if (listItem.termType === "BlankNode") { + //Look for special types + let zeroOrMorePathObjects = getObjects( + shapeStore, + listItem, + SHACL.zeroOrMorePath, + null, + ); + let oneOrMorePathObjects = getObjects( + shapeStore, + listItem, + SHACL.oneOrMorePath, + null, + ); + let zeroOrOnePathObjects = getObjects( + shapeStore, + listItem, + SHACL.zeroOrOnePath, + null, + ); + let inversePathObjects = getObjects( + shapeStore, + listItem, + SHACL.inversePath, + null, + ); + let alternativePathObjects = getObjects( + shapeStore, + listItem, + SHACL.alternativePath, + null, + ); + if (zeroOrMorePathObjects[0]) { + return new ZeroOrMorePath( + this.constructPathPattern(shapeStore, zeroOrMorePathObjects[0]), + ); + } else if (oneOrMorePathObjects[0]) { + return new OneOrMorePath( + this.constructPathPattern(shapeStore, oneOrMorePathObjects[0]), + ); + } else if (zeroOrOnePathObjects[0]) { + return new ZeroOrOnePath( + this.constructPathPattern(shapeStore, zeroOrOnePathObjects[0]), + ); + } else if (inversePathObjects[0]) { + return new InversePath( + this.constructPathPattern(shapeStore, inversePathObjects[0]), + ); + } else if (alternativePathObjects[0]) { + let alternativeListArray = this.rdfListToArray( + shapeStore, + alternativePathObjects[0], + ).map((value: Term) => { + return this.constructPathPattern(shapeStore, value); + }); + return new AlternativePath(alternativeListArray); + } else { + const items = this.rdfListToArray(shapeStore, listItem); + return new SequencePath( + items.map((x) => this.constructPathPattern(shapeStore, x)), + ); + } + } + + return new PredicatePath(listItem); + } + + /** + * @param shapeStore + * @param propertyShapeId + * @param shape + * @param required + * @returns false if it wasn't a property shape + */ + protected preprocessPropertyShape( + shapeStore: RdfStore, + propertyShapeId: Term, + shape: ShapeTemplate, + required?: boolean, + ): boolean { + //Skip if shape has been deactivated + let deactivated = getObjects( + shapeStore, + propertyShapeId, + SHACL.deactivated, + null, + ); + if (deactivated.length > 0 && deactivated[0].value === "true") { + 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]; + //Process the path now and make sure there's a match function + if (!path) { + return false; //this isn't a property shape... + } + + let pathPattern = this.constructPathPattern(shapeStore, path); + + let minCount = getObjects( + shapeStore, + propertyShapeId, + SHACL.minCount, + null, + ); + + if ((minCount[0] && minCount[0].value !== "0") || required) { + shape.requiredPaths.push(pathPattern); + } else { + //TODO: don't include node links? + shape.optionalPaths.push(pathPattern); + } + // **TODO**: will the sh:or, sh:xone, sh:and, etc. be of use here? It won't contain any more information about possible properties? + // 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); + if (nodeLink[0]) { + shape.nodeLinks.push(new NodeLink(pathPattern, nodeLink[0])); + } + //TODO: Can Nodelinks appear in conditionals from here? Probably they can? (same comment as ↑) + return true; // Success: the property shape has been processed + } + + /** + * Processes a NodeShape or PropertyShape and adds NodeLinks and required properties to the arrays. + * @param shapeStore + * @param shapeId + * @param shape + * @returns + */ + preprocessShape(shapeStore: RdfStore, shapeId: Term, shape: ShapeTemplate) { + return this.preprocessPropertyShape(shapeStore, shapeId, shape) + ? true + : this.preprocessNodeShape(shapeStore, shapeId, shape); + } + + /** + * Processes a NodeShape + * @param shapeStore + * @param nodeShapeId + * @param shape + */ + protected preprocessNodeShape( + shapeStore: RdfStore, + nodeShapeId: Term, + shape: ShapeTemplate, + ) { + // Extract label + const rdfsLabel = getObjects(shapeStore, nodeShapeId, RDFS.terms.label)[0]; + if (rdfsLabel) { + shape.label = rdfsLabel.value; + } else { + shape.label = nodeShapeId.termType === "BlankNode" ? + nodeShapeId.value : + nodeShapeId.value.split("/")[nodeShapeId.value.split("/").length - 1]; + } + + //Check if it's closed or open + let closedIndicator: Term = getObjects( + shapeStore, + nodeShapeId, + SHACL.closed, + null, + )[0]; + if (closedIndicator && closedIndicator.value === "true") { + shape.closed = true; + } + + //Process properties if it has any + 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)) { + // 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)) { + this.preprocessShape(shapeStore, and, shape); + } + } + //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))) { + let atLeastOneList: Array = this.rdfListToArray( + shapeStore, + xoneOrOrList, + ).map((val): ShapeTemplate => { + let newShape = new ShapeTemplate(); + //Create a new shape and process as usual -- but mind that we don't trigger a circular shape here... + this.preprocessShape(shapeStore, val, newShape); + return newShape; + //Add this one to the shapesgraph + }); + shape.atLeastOneLists.push(atLeastOneList); + } + //And finally, we're just ignoring sh:not. Don't process this one + } + + /** + * @param shapeStore + */ + initializeFromStore(shapeStore: RdfStore): RDFMap { + //get all named nodes of entities that are sh:NodeShapes which we'll recognize through their use of sh:property (we'll find other relevant shape nodes later on) + //TODO: This is a limitation though: we only support NodeShapes with at least one sh:property set? Other NodeShapes in this context are otherwise just meaningless? + 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)) + //DISTINCT + .filter((value: Term, index: number, array: Array) => { + return array.findIndex((x) => x.equals(value)) === index; + }); + + let shapes = new RDFMap(); + for (let shapeId of shapeNodes) { + let shape = new ShapeTemplate(); + //Don't process if shape is deactivated + 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); + } + } + return shapes; + } + + /** + * Processes all element from an RDF List, or detects it wasn't a list after all and it's just one element. + * @param shapeStore + * @param item + * @returns + */ + protected* rdfListToGenerator( + shapeStore: RdfStore, + item: Term, + ): Generator { + if ( + getObjects( + shapeStore, + item, + df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), + null, + )[0] + ) { + yield getObjects( + shapeStore, + item, + df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), + null, + )[0]; + let rest = getObjects( + shapeStore, + item, + df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), + null, + )[0]; + while ( + rest && + rest.value !== "http://www.w3.org/1999/02/22-rdf-syntax-ns#nil" + ) { + yield getObjects( + shapeStore, + rest, + df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#first"), + null, + )[0]; + rest = getObjects( + shapeStore, + rest, + df.namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest"), + null, + )[0]; + } + } else { + // It's not a list. It's just one element. + yield item; + } + return; + } + + protected rdfListToArray(shapeStore: RdfStore, item: Term): Array { + return Array.from(this.rdfListToGenerator(shapeStore, item)); + } +} + +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, +) { + return store.getQuads(subject, predicate, null, graph).map((quad) => { + return quad.object; + }); +}; diff --git a/package-lock.json b/package-lock.json index 895551d..e7b3b16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "extract-cbd-shape", - "version": "0.1.2", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "extract-cbd-shape", - "version": "0.1.2", + "version": "0.1.5", "license": "MIT", "dependencies": { "@treecg/types": "^0.4.5", @@ -23,10 +23,13 @@ "@types/debug": "^4.1.12", "@types/mocha": "^10.0.1", "@types/n3": "^1.16.1", + "@types/pako": "^2.0.3", "@types/sinon": "^10.0.16", "benchmark": "^2.1.4", "chai": "^4.3.7", + "js-base64": "^3.7.7", "mocha": "^10.2.0", + "pako": "^2.1.0", "rollup-plugin-polyfill-node": "^0.12.0", "systeminformation": "^5.21.20", "ts-node": "^10.9.1" @@ -645,6 +648,12 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/pako": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz", + "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", + "dev": true + }, "node_modules/@types/readable-stream": { "version": "2.3.15", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", @@ -1642,6 +1651,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2057,6 +2072,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "dev": true + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", diff --git a/package.json b/package.json index 75b0937..c7073f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "extract-cbd-shape", - "version": "0.1.5", + "version": "0.1.6", "description": "Extract an entity based on CBD and a SHACL shape", "main": "dist/lib/extract-cbd-shape.js", "types": "dist/lib/extract-cbd-shape.d.ts", @@ -33,10 +33,13 @@ "@types/debug": "^4.1.12", "@types/mocha": "^10.0.1", "@types/n3": "^1.16.1", + "@types/pako": "^2.0.3", "@types/sinon": "^10.0.16", "benchmark": "^2.1.4", "chai": "^4.3.7", + "js-base64": "^3.7.7", "mocha": "^10.2.0", + "pako": "^2.1.0", "rollup-plugin-polyfill-node": "^0.12.0", "systeminformation": "^5.21.20", "ts-node": "^10.9.1" diff --git a/tests/01 - fetching a shacl shape/shapeTemplate.test.ts b/tests/01 - fetching a shacl shape/shapeTemplate.test.ts index 6e3b1e5..1dc0609 100644 --- a/tests/01 - fetching a shacl shape/shapeTemplate.test.ts +++ b/tests/01 - fetching a shacl shape/shapeTemplate.test.ts @@ -1,6 +1,6 @@ -import { ShapesGraph } from "../../lib/Shape"; import rdfDereference from "rdf-dereference"; import { RdfStore } from "rdf-stores"; +import {ShapesGraph} from "../../lib/ShapesGraph"; describe("Test shape template of the SHACL SHACL", function () { let shapeStore = RdfStore.createDefault(); let shapesGraph: ShapesGraph; diff --git a/tests/04 - logical edge cases/testShapeTemplate.test.ts b/tests/04 - logical edge cases/testShapeTemplate.test.ts index 7c3ca31..800bb7a 100644 --- a/tests/04 - logical edge cases/testShapeTemplate.test.ts +++ b/tests/04 - logical edge cases/testShapeTemplate.test.ts @@ -8,8 +8,9 @@ import { Writer, } from "n3"; import { RdfStore } from "rdf-stores"; -import { ShapesGraph, ShapeTemplate } from "../../lib/Shape"; +import { ShapeTemplate } from "../../lib/Shape"; import rdfDereference from "rdf-dereference"; +import {ShapesGraph} from "../../lib/ShapesGraph"; const { namedNode } = DataFactory; describe("Test shape template of the logical edge cases", function () { diff --git a/tests/05 - paths/pathPattern.test.ts b/tests/05 - paths/pathPattern.test.ts index f457366..a405728 100644 --- a/tests/05 - paths/pathPattern.test.ts +++ b/tests/05 - paths/pathPattern.test.ts @@ -1,10 +1,10 @@ import { assert } from "chai"; import { DataFactory, NamedNode, Store } from "n3"; -import { ShapesGraph } from "../../lib/Shape"; import rdfDereference from "rdf-dereference"; import { CbdExtracted } from "../../lib/CBDShapeExtractor"; const { namedNode } = DataFactory; import { RdfStore } from "rdf-stores"; +import {ShapesGraph} from "../../lib/ShapesGraph"; describe("Test whether the Patterns are correctly created", function () { let shapeStore = RdfStore.createDefault(); let shapesGraph: ShapesGraph; diff --git a/tests/05 - paths/shapeTemplate.test.ts b/tests/05 - paths/shapeTemplate.test.ts index 4115787..7a2df0f 100644 --- a/tests/05 - paths/shapeTemplate.test.ts +++ b/tests/05 - paths/shapeTemplate.test.ts @@ -1,8 +1,9 @@ import { assert } from "chai"; import { DataFactory } from "rdf-data-factory"; import { RdfStore } from "rdf-stores"; -import { ShapesGraph, ShapeTemplate } from "../../lib/Shape"; +import { ShapeTemplate } from "../../lib/Shape"; import rdfDereference from "rdf-dereference"; +import {ShapesGraph} from "../../lib/ShapesGraph"; const df = new DataFactory(); diff --git a/tests/07 - mermaid/all-together-path.txt b/tests/07 - mermaid/all-together-path.txt new file mode 100644 index 0000000..9071cfa --- /dev/null +++ b/tests/07 - mermaid/all-together-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((AllTogetherPathShape)) + S1_0[ ]-->|"http://example.org/p1|http://example.org/p2"|S1 diff --git a/tests/07 - mermaid/alternative-path.txt b/tests/07 - mermaid/alternative-path.txt new file mode 100644 index 0000000..c2b6466 --- /dev/null +++ b/tests/07 - mermaid/alternative-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((AlternativePathShape)) + S1-->|"http://example.org/p1|http://example.org/p2"|S1_0[ ] diff --git a/tests/07 - mermaid/double-inverse-path.txt b/tests/07 - mermaid/double-inverse-path.txt new file mode 100644 index 0000000..3000b46 --- /dev/null +++ b/tests/07 - mermaid/double-inverse-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((DoubleInversePathShape)) + S1-->|"http://example.org/p1"|S1_0[ ] diff --git a/tests/07 - mermaid/inverse-path.txt b/tests/07 - mermaid/inverse-path.txt new file mode 100644 index 0000000..1590583 --- /dev/null +++ b/tests/07 - mermaid/inverse-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((InversePathShape)) + S1_0[ ]-->|"http://example.org/p2"|S1 diff --git a/tests/07 - mermaid/mermaid.test.ts b/tests/07 - mermaid/mermaid.test.ts new file mode 100644 index 0000000..46f1c82 --- /dev/null +++ b/tests/07 - mermaid/mermaid.test.ts @@ -0,0 +1,144 @@ +import { assert } from "chai"; +import { DataFactory } from "rdf-data-factory"; +import { RdfStore } from "rdf-stores"; +import rdfDereference from "rdf-dereference"; +import {ShapesGraph} from "../../lib/ShapesGraph"; +import fs from "fs/promises"; + +const df = new DataFactory(); + +describe("Test whether the correct Mermaid text is generated for a ShapesGraph", function () { + let shapeStore = RdfStore.createDefault(); + let shapesGraph: ShapesGraph; + before(async () => { + let readStream = ( + await rdfDereference.dereference("./tests/07 - mermaid/shape.ttl", { + localFiles: true, + }) + ).data; + + await new Promise((resolve, reject) => { + shapeStore.import(readStream).on("end", resolve).on("error", reject); + }); + + shapesGraph = new ShapesGraph(shapeStore); + }); + + it("Sequence path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/SequencePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/sequence-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Optional sequence path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/OptionalSequencePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/optional-sequence-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Inverse path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/InversePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/inverse-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Optional inverse path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/OptionalInversePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/optional-inverse-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Sequence and inverse path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/SequenceAndInversePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/sequence-and-inverse-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Double inverse path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/DoubleInversePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/double-inverse-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Triple inverse path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/TripleInversePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/triple-inverse-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Quadruple inverse path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/QuadrupleInversePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/quadruple-inverse-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Zero or more path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/ZeroOrMorePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/zero-or-more-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("One or more path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/OneOrMorePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/one-or-more-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Zero or one path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/ZeroOrOnePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/zero-or-one-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Alternative path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/AlternativePathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/alternative-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("All together path", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/AllTogetherPathShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/all-together-path.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Nested shape", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/NestedShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/nested-shape.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Nested with optional path shape", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/NestedWithOptionalShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/nested-with-optional-shape.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Xone with node shape", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/XoneWithNodeShape")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/xone-with-node-shape.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Xone with node shape 2", async () => { + const actualMermaid = shapesGraph.toMermaid(df.namedNode("http://example.org/XoneWithNodeShape2")); + const expectedMermaid = await fs.readFile('./tests/07 - mermaid/xone-with-node-shape-2.txt', 'utf-8'); + assert.equal(actualMermaid, expectedMermaid); + }); + + it("Throw error when shape not found", async () => { + let error: Error; + const term = "http://example.org/abc"; + + try { + shapesGraph.toMermaid(df.namedNode(term)); + } catch (e) { + error = e as Error; + } + + // @ts-ignore + assert.isDefined(error); + // @ts-ignore + assert.equal(error.message, `No shape found for term "${term}"`); + }); +}); diff --git a/tests/07 - mermaid/nested-shape.txt b/tests/07 - mermaid/nested-shape.txt new file mode 100644 index 0000000..168db29 --- /dev/null +++ b/tests/07 - mermaid/nested-shape.txt @@ -0,0 +1,5 @@ +flowchart LR + S1((NestedShape)) + S1-->|"http://example.org/subject"|S1_0[ ] + S1_0((SecondNestedShape)) + S1_0-->|"http://www.w3.org/2000/01/rdf-schema#label"|S1_0_1[ ] diff --git a/tests/07 - mermaid/nested-with-optional-shape.txt b/tests/07 - mermaid/nested-with-optional-shape.txt new file mode 100644 index 0000000..6aa3d10 --- /dev/null +++ b/tests/07 - mermaid/nested-with-optional-shape.txt @@ -0,0 +1,5 @@ +flowchart LR + S1((NestedWithOptionalShape)) + S1-.->|"http://example.org/subject"|S1_0[ ] + S1_0((OptionalNestedNodeShape)) + S1_0-->|"http://www.w3.org/2000/01/rdf-schema#label"|S1_0_1[ ] diff --git a/tests/07 - mermaid/one-or-more-path.txt b/tests/07 - mermaid/one-or-more-path.txt new file mode 100644 index 0000000..7eaad51 --- /dev/null +++ b/tests/07 - mermaid/one-or-more-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((OneOrMorePathShape)) + S1-->|"http://example.org/p1+"|S1_0[ ] diff --git a/tests/07 - mermaid/optional-inverse-path.txt b/tests/07 - mermaid/optional-inverse-path.txt new file mode 100644 index 0000000..c28a8cf --- /dev/null +++ b/tests/07 - mermaid/optional-inverse-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((OptionalInversePathShape)) + S1_0[ ]-.->|"http://example.org/p2"|S1 diff --git a/tests/07 - mermaid/optional-sequence-path.txt b/tests/07 - mermaid/optional-sequence-path.txt new file mode 100644 index 0000000..8fd5650 --- /dev/null +++ b/tests/07 - mermaid/optional-sequence-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((OptionalSequencePathShape)) + S1-.->|"http://example.org/p1/http://example.org/p2"|S1_0[ ] diff --git a/tests/07 - mermaid/quadruple-inverse-path.txt b/tests/07 - mermaid/quadruple-inverse-path.txt new file mode 100644 index 0000000..010a5a9 --- /dev/null +++ b/tests/07 - mermaid/quadruple-inverse-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((QuadrupleInversePathShape)) + S1-->|"http://example.org/p1"|S1_0[ ] diff --git a/tests/07 - mermaid/sequence-and-inverse-path.txt b/tests/07 - mermaid/sequence-and-inverse-path.txt new file mode 100644 index 0000000..bff4d5f --- /dev/null +++ b/tests/07 - mermaid/sequence-and-inverse-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((SequenceAndInversePathShape)) + S1_0[ ]-->|"http://example.org/p2/http://example.org/p1"|S1 diff --git a/tests/07 - mermaid/sequence-path.txt b/tests/07 - mermaid/sequence-path.txt new file mode 100644 index 0000000..3381ea5 --- /dev/null +++ b/tests/07 - mermaid/sequence-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((SequencePathShape)) + S1-->|"http://example.org/p1/http://example.org/p2"|S1_0[ ] diff --git a/tests/07 - mermaid/shape.ttl b/tests/07 - mermaid/shape.ttl new file mode 100644 index 0000000..287a0b0 --- /dev/null +++ b/tests/07 - mermaid/shape.ttl @@ -0,0 +1,166 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . +@prefix foaf: . +@prefix ex: . + +# Sequence path +ex:SequencePathShape a sh:NodeShape ; + sh:property [ + sh:path (ex:p1 ex:p2 ) ; + sh:minCount 1 + ] . + +# Optional sequence path +ex:OptionalSequencePathShape a sh:NodeShape ; + sh:property [ + sh:path (ex:p1 ex:p2 ) ; + sh:minCount 0 + ] . + +# Inverse path +ex:InversePathShape a sh:NodeShape ; + sh:property [ + sh:path [sh:inversePath ex:p2 ] ; + sh:minCount 1 + ] . + +# Optional inverse path +ex:OptionalInversePathShape a sh:NodeShape ; + sh:property [ + sh:path [sh:inversePath ex:p2 ] ; + sh:minCount 0 + ] . + +# Sequence and inverse path +ex:SequenceAndInversePathShape a sh:NodeShape ; + sh:property [ + sh:path ([sh:inversePath ex:p2 ] ex:p1 ) ; + sh:minCount 1 + ] . + +# Double inverse path +ex:DoubleInversePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:inversePath [sh:inversePath ex:p1 ] ]) ; + sh:minCount 1 + ] . + +# Triple inverse path +ex:TripleInversePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:inversePath [sh:inversePath [sh:inversePath ex:p1 ] ] ]) ; + sh:minCount 1 + ] . + +# Quadruple inverse path +ex:QuadrupleInversePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:inversePath [sh:inversePath [sh:inversePath [sh:inversePath ex:p1 ] ] ] ]) ; + sh:minCount 1 + ] . + +# Zero or More path +ex:ZeroOrMorePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:zeroOrMorePath ex:p1 ]) ; + sh:minCount 1 + ] . + +ex:OneOrMorePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:oneOrMorePath ex:p1 ]) ; + sh:minCount 1 + ] . + +ex:ZeroOrOnePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path (ex:p1 [sh:zeroOrOnePath ex:p2 ]) ; + sh:minCount 1 + ] . + +ex:AlternativePathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:alternativePath (ex:p1 ex:p2)]) ; + sh:minCount 1 + ] . + + +ex:AllTogetherPathShape a sh:NodeShape ; + sh:closed true; + sh:property [ + sh:path ([sh:alternativePath ([ sh:inversePath ex:p1 ] ex:p2)]) ; + sh:minCount 1 + ] . + +# Nested shape +ex:NestedShape a sh:NodeShape ; + sh:property [ + sh:path ex:subject ; + sh:minCount 1 ; + sh:node [ + rdfs:label "SecondNestedShape" ; + sh:property [ + sh:path rdfs:label ; + sh:minCount 1 + ] + ] + ] . + +ex:XoneWithNodeShape a sh:NodeShape ; + sh:xone ( [ + sh:path foaf:name; + sh:minCount 1 + ] [ + rdfs:label "SecondXoneWithNodeShape" ; + sh:property [ + sh:path ex:qualifiedName ; + sh:node ex:QualifiedNameShape; + sh:minCount 1 + ] + ]) . + + +ex:XoneWithNodeShape2 a sh:NodeShape ; + sh:xone ( [ + sh:path foaf:name; + sh:minCount 1 + ] [ + rdfs:label "SecondXoneWithNodeShape2" ; + sh:property [ + sh:path ex:qualifiedName ; + sh:node ex:QualifiedNameShape2; + sh:minCount 1 + ] + ]) . + +ex:QualifiedNameShape2 a sh:NodeShape ; + sh:property [ + sh:minCount 1 ; + sh:path ex:name ; + ], [ + sh:path ex:validUntil ; + # ... + ] . + +# Nested shape with optional path +ex:NestedWithOptionalShape a sh:NodeShape ; + sh:property [ + sh:path ex:subject ; + sh:minCount 0 ; + sh:node [ + rdfs:label "OptionalNestedNodeShape" ; + sh:property [ + sh:path rdfs:label ; + sh:minCount 1 + ] + ] + ] . diff --git a/tests/07 - mermaid/triple-inverse-path.txt b/tests/07 - mermaid/triple-inverse-path.txt new file mode 100644 index 0000000..e6a607b --- /dev/null +++ b/tests/07 - mermaid/triple-inverse-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((TripleInversePathShape)) + S1_0[ ]-->|"http://example.org/p1"|S1 diff --git a/tests/07 - mermaid/xone-with-node-shape-2.txt b/tests/07 - mermaid/xone-with-node-shape-2.txt new file mode 100644 index 0000000..3bcfda4 --- /dev/null +++ b/tests/07 - mermaid/xone-with-node-shape-2.txt @@ -0,0 +1,12 @@ +flowchart LR + S1((XoneWithNodeShape2)) + S1---X1_0{OR} + X1_0---S1_0 + S1_0((Shape)) + S1_0-->|"http://xmlns.com/foaf/0.1/name"|S1_0_1[ ] + X1_0---S1_2 + S1_2((SecondXoneWithNodeShape2)) + S1_2-->|"http://example.org/qualifiedName"|S1_2_3[ ] + S1_2_3((QualifiedNameShape2)) + S1_2_3-->|"http://example.org/name"|S1_2_3_4[ ] + S1_2_3-.->|"http://example.org/validUntil"|S1_2_3_5[ ] diff --git a/tests/07 - mermaid/xone-with-node-shape.txt b/tests/07 - mermaid/xone-with-node-shape.txt new file mode 100644 index 0000000..edc6771 --- /dev/null +++ b/tests/07 - mermaid/xone-with-node-shape.txt @@ -0,0 +1,10 @@ +flowchart LR + S1((XoneWithNodeShape)) + S1---X1_0{OR} + X1_0---S1_0 + S1_0((Shape)) + S1_0-->|"http://xmlns.com/foaf/0.1/name"|S1_0_1[ ] + X1_0---S1_2 + S1_2((SecondXoneWithNodeShape)) + S1_2-->|"http://example.org/qualifiedName"|S1_2_3[ ] + S1_2_3((QualifiedNameShape)) diff --git a/tests/07 - mermaid/zero-or-more-path.txt b/tests/07 - mermaid/zero-or-more-path.txt new file mode 100644 index 0000000..b9b55cd --- /dev/null +++ b/tests/07 - mermaid/zero-or-more-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((ZeroOrMorePathShape)) + S1-->|"http://example.org/p1*"|S1_0[ ] diff --git a/tests/07 - mermaid/zero-or-one-path.txt b/tests/07 - mermaid/zero-or-one-path.txt new file mode 100644 index 0000000..afdb141 --- /dev/null +++ b/tests/07 - mermaid/zero-or-one-path.txt @@ -0,0 +1,3 @@ +flowchart LR + S1((ZeroOrOnePathShape)) + S1-->|"http://example.org/p1/http://example.org/p2?"|S1_0[ ]