@jeswr/shacl2shex
Version:
Convert SHACL to ShEx
323 lines (322 loc) • 15.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeShapeMap = exports.shapeMapFromDataset = void 0;
exports.shaclStoreToShexSchema = shaclStoreToShexSchema;
exports.writeShexSchema = writeShexSchema;
const writer_1 = __importDefault(require("@shexjs/writer"));
const n3_1 = require("n3");
const rdf_namespaces_1 = require("rdf-namespaces");
const Shacl_shapeTypes_1 = require("./ldo/Shacl.shapeTypes");
const shapeFromDataset_1 = require("./shapeFromDataset");
var shapeMapFromDataset_1 = require("./shapeMapFromDataset");
Object.defineProperty(exports, "shapeMapFromDataset", { enumerable: true, get: function () { return shapeMapFromDataset_1.shapeMapFromDataset; } });
Object.defineProperty(exports, "writeShapeMap", { enumerable: true, get: function () { return shapeMapFromDataset_1.writeShapeMap; } });
const { namedNode, defaultGraph } = n3_1.DataFactory;
function getSingleObjectOfType(store, subject, predicate) {
const objects = [...store.match(subject, predicate, null, defaultGraph())];
if (objects.length !== 1) {
console.warn('Expected exactly one object', objects);
return undefined;
}
if (objects[0].object.termType !== 'NamedNode') {
console.warn('Expected object to be a NamedNode', objects[0]);
return undefined;
}
return objects[0].object;
}
async function shaclStoreToShexSchema(shapeStore) {
const shexShapes = [];
// First pass: collect all shapes and their target classes for reference resolution
const shapeTargetMap = new Map(); // target class -> shape IRI
for (const { subject: shape } of shapeStore.match(null, namedNode(rdf_namespaces_1.rdf.type), namedNode(rdf_namespaces_1.shacl.NodeShape), defaultGraph())) {
const targetClasses = shapeStore.getObjects(shape, namedNode(rdf_namespaces_1.shacl.targetClass), defaultGraph());
for (const targetClass of targetClasses) {
if (targetClass.termType === 'NamedNode') {
shapeTargetMap.set(targetClass.value, shape.value);
}
}
}
for (const { subject: shape } of shapeStore.match(null, namedNode(rdf_namespaces_1.rdf.type), namedNode(rdf_namespaces_1.shacl.NodeShape), defaultGraph())) {
// Extract shape-level constraints
const shapeNodeKind = shapeStore.getObjects(shape, namedNode(rdf_namespaces_1.shacl.nodeKind), defaultGraph());
const eachOf = [];
for (const property of shapeStore.getObjects(shape, namedNode(rdf_namespaces_1.shacl.property), defaultGraph())) {
if (property.termType !== 'NamedNode' && property.termType !== 'BlankNode') {
console.warn('Unsupported property', property);
continue;
}
const shapeData = (0, shapeFromDataset_1.shapeFromDataset)(Shacl_shapeTypes_1.ShapeShapeShapeType, shapeStore, property);
const inValues = shapeStore.getObjects(property, namedNode(rdf_namespaces_1.shacl.in__workaround), defaultGraph());
const shapeRef = shapeStore.getObjects(property, namedNode(rdf_namespaces_1.shacl.node), defaultGraph());
const path = shapeStore.getObjects(property, namedNode(rdf_namespaces_1.shacl.path), defaultGraph());
let valueExpr = {
type: 'NodeConstraint',
};
if (shapeData.nodeKind) {
valueExpr.nodeKind = {
IRI: 'iri',
Literal: 'literal',
BlankNode: 'bnode',
BlankNodeOrIRI: 'nonliteral',
IRIOrLiteral: undefined,
BlankNodeOrLiteral: undefined,
}[shapeData.nodeKind['@id']];
}
if (inValues.length === 1) {
const list = shapeStore.extractLists()[inValues[0].value];
if (list) {
const [firstTerm] = list;
// TODO: Make this just the else case once https://github.com/o-development/ldo/issues/31 is resolved
if (firstTerm
&& firstTerm.termType === 'Literal'
&& list.every((v) => v.termType === 'Literal' && v.datatype.equals(firstTerm.datatype))) {
valueExpr.datatype = firstTerm.datatype.value;
}
else {
valueExpr.values = list.map((v) => (v.termType === 'Literal' ? {
value: v.value,
type: v.datatype.value,
} : v.value));
}
}
}
if (shapeData.datatype) {
valueExpr.datatype = shapeData.datatype['@id'];
}
else if (shapeData.class && shapeData.class.length > 0) {
// Check if there's a shape that targets this class
const classIri = shapeData.class[0]['@id'];
const targetShape = shapeTargetMap.get(classIri);
if (targetShape && shapeData.class.length === 1) {
// Use shape reference if there's a shape targeting this class
if (valueExpr.nodeKind) {
// Combine nodeKind constraint with shape reference
valueExpr = {
type: 'ShapeAnd',
shapeExprs: [
{
type: 'NodeConstraint',
nodeKind: valueExpr.nodeKind,
},
targetShape,
],
};
}
else {
// Just the shape reference
valueExpr = targetShape;
}
}
else
// Handle sh:class constraint by creating a nested shape with rdf:type
// If there's also a nodeKind constraint, we need to combine them
if (valueExpr.nodeKind) {
// Create a ShapeAnd that combines the nodeKind constraint with the class shape
valueExpr = {
type: 'ShapeAnd',
shapeExprs: [
{
type: 'NodeConstraint',
nodeKind: valueExpr.nodeKind,
},
{
type: 'Shape',
expression: {
type: 'TripleConstraint',
predicate: rdf_namespaces_1.rdf.type,
valueExpr: {
type: 'NodeConstraint',
values: shapeData.class.map((cls) => cls['@id']),
},
},
},
],
};
}
else {
// Just the class constraint without nodeKind
valueExpr = {
type: 'Shape',
expression: {
type: 'TripleConstraint',
predicate: rdf_namespaces_1.rdf.type,
valueExpr: {
type: 'NodeConstraint',
values: shapeData.class.map((cls) => cls['@id']),
},
},
};
}
}
else if (shapeRef.length === 1) {
// TODO: Error if there are any other constraints
valueExpr = shapeRef[0].value;
}
else if ((typeof valueExpr === 'object' && 'nodeKind' in valueExpr && valueExpr.nodeKind)
|| (typeof valueExpr === 'object' && 'values' in valueExpr && valueExpr.values)) {
// Noop - keep the existing NodeConstraint
}
else {
console.warn('Unsupported property', property);
continue;
}
function toTripleConstraint(pathElem) {
if (pathElem.termType === 'NamedNode') {
return {
type: 'TripleConstraint',
predicate: pathElem.value,
valueExpr,
// FIXME: Int checks etc should be done here
min: shapeData.minCount ?? 0,
max: shapeData.maxCount ?? -1,
};
}
const inversePath = getSingleObjectOfType(shapeStore, pathElem, namedNode(rdf_namespaces_1.shacl.inversePath));
if (inversePath) {
return {
type: 'TripleConstraint',
predicate: pathElem.value,
valueExpr,
inverse: true,
// FIXME: Int checks etc should be done here
min: shapeData.minCount ?? 0,
max: shapeData.maxCount ?? -1,
};
}
const oneOrMorePath = getSingleObjectOfType(shapeStore, pathElem, namedNode(rdf_namespaces_1.shacl.oneOrMorePath));
if (oneOrMorePath) {
return {
type: 'TripleConstraint',
predicate: pathElem.value,
valueExpr: {
type: 'ShapeOr',
shapeExprs: [{
type: 'Shape',
expression: toTripleConstraint(oneOrMorePath),
}, valueExpr],
},
min: shapeData.minCount ?? 0,
max: shapeData.maxCount ?? -1,
};
}
console.log(shape, getSingleObjectOfType(shapeStore, pathElem, namedNode('http://www.w3.org/ns/shacl#alternativePath')), getSingleObjectOfType(shapeStore, pathElem, namedNode('http://www.w3.org/ns/shacl#zeroOrMorePath')), getSingleObjectOfType(shapeStore, pathElem, namedNode('http://www.w3.org/ns/shacl#oneOrMorePath')), getSingleObjectOfType(shapeStore, pathElem, namedNode('http://www.w3.org/ns/shacl#zeroOrOnePath')), getSingleObjectOfType(shapeStore, pathElem, namedNode('http://www.w3.org/ns/shacl#inversePath')), shapeStore.extractLists()[pathElem.value]);
throw new Error('Unsupported path');
}
try {
eachOf.push(toTripleConstraint(path[0]));
}
catch (e) {
console.warn('Error processing property', property, e);
}
}
// Handle shape-level nodeKind constraint
let shapeNodeKindConstraint;
if (shapeNodeKind.length === 1 && shapeNodeKind[0].termType === 'NamedNode') {
const nodeKindValue = shapeNodeKind[0].value;
const nodeKindMapping = {
'http://www.w3.org/ns/shacl#IRI': 'iri',
'http://www.w3.org/ns/shacl#Literal': 'literal',
'http://www.w3.org/ns/shacl#BlankNode': 'bnode',
'http://www.w3.org/ns/shacl#BlankNodeOrIRI': 'nonliteral',
};
if (nodeKindValue in nodeKindMapping) {
shapeNodeKindConstraint = nodeKindMapping[nodeKindValue];
}
}
// Create the shape expression
let shapeExpr;
if (eachOf.length === 0) {
// If there are no properties but there is a nodeKind constraint, create a simple nodeKind constraint
if (shapeNodeKindConstraint) {
shapeExpr = {
type: 'NodeConstraint',
nodeKind: shapeNodeKindConstraint,
};
}
else {
console.warn('No properties found in shape', shape);
continue;
}
}
else {
// If there are properties, create the shape with properties
const shapeWithProperties = {
type: 'Shape',
expression: {
type: 'EachOf',
expressions: eachOf,
},
};
// If there's a nodeKind constraint, wrap with ShapeAnd
if (shapeNodeKindConstraint) {
shapeExpr = {
type: 'ShapeAnd',
shapeExprs: [
{
type: 'NodeConstraint',
nodeKind: shapeNodeKindConstraint,
},
shapeWithProperties,
],
};
}
else {
shapeExpr = shapeWithProperties;
}
}
shexShapes.push({
id: shape.value,
type: 'ShapeDecl',
shapeExpr,
});
}
const filteredShapes = [];
const shapes = new Set(shexShapes.map((s) => s.id));
// TODO: Apply this recursively
// TODO: Add warnings
for (const shape of shexShapes) {
let shouldInclude = true;
// Handle different shape types
if (shape.shapeExpr.type === 'NodeConstraint') {
// NodeConstraint shapes are always included
shouldInclude = true;
}
else if (shape.shapeExpr.type === 'ShapeAnd') {
// ShapeAnd shapes need to check their component shapes
// For now, we'll include them all, but this could be refined
shouldInclude = true;
}
else if (shape.shapeExpr.type === 'Shape'
&& shape.shapeExpr?.expression
&& typeof shape.shapeExpr?.expression === 'object'
&& shape.shapeExpr?.expression.type === 'EachOf'
&& shape.shapeExpr?.expression.expressions) {
// Original logic for Shape with EachOf expressions
shape.shapeExpr.expression.expressions = shape.shapeExpr.expression.expressions.filter((eachOf) => typeof eachOf !== 'string'
&& eachOf.type === 'TripleConstraint'
&& ((typeof eachOf.valueExpr !== 'string') || shapes.has(eachOf.valueExpr)));
shouldInclude = shape.shapeExpr.expression.expressions.length > 0;
}
if (shouldInclude) {
filteredShapes.push(shape);
}
}
return {
type: 'Schema',
shapes: filteredShapes,
};
}
function writeShexSchema(schema, prefixes) {
const shexWriter = new writer_1.default({ prefixes }, {});
return new Promise((resolve, reject) => {
shexWriter.writeSchema(schema, (error, text) => {
if (error)
reject(error);
else if (text !== undefined)
resolve(text);
});
});
}