WIP
This commit is contained in:
153
src/runner/spec.js
Normal file
153
src/runner/spec.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import N3 from 'n3';
|
||||
|
||||
// Parse a spec (rep:ReportSpec + sh:NodeShape) into a plain object.
|
||||
// No validation here; that is check.js.
|
||||
|
||||
const SH = 'http://www.w3.org/ns/shacl#';
|
||||
const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
|
||||
const REP = 'http://mu.semte.ch/vocabularies/reporting/';
|
||||
const RDFS_LABEL = 'http://www.w3.org/2000/01/rdf-schema#label';
|
||||
|
||||
export function parseSpec(turtle) {
|
||||
const parser = new N3.Parser();
|
||||
const quads = parser.parse(turtle);
|
||||
const store = new N3.Store(quads);
|
||||
|
||||
const specNode = store.getQuads(null, RDF + 'type', REP + 'ReportSpec')[0]?.subject;
|
||||
if (!specNode) throw new Error('spec has no rep:ReportSpec');
|
||||
|
||||
const one = (s, pred) => store.getQuads(s, pred, null)[0]?.object || null;
|
||||
|
||||
const targetClass = one(specNode, SH + 'targetClass')?.value || null;
|
||||
const profileUri = one(specNode, REP + 'profile')?.value || null;
|
||||
const title = one(specNode, 'http://purl.org/dc/terms/title')?.value || null;
|
||||
|
||||
const filters = store.getQuads(specNode, SH + 'property', null).map(q =>
|
||||
parseFilter(store, q.object, { withLabel: true }));
|
||||
|
||||
const columnsQuad = one(specNode, REP + 'columns');
|
||||
const columns = columnsQuad ? parseColumnList(store, columnsQuad) : [];
|
||||
|
||||
return { uri: specNode.value, title, profileUri, targetClass, filters, columns };
|
||||
}
|
||||
|
||||
function parseFilter(store, p, { withLabel = false } = {}) {
|
||||
let path = [];
|
||||
const pathQuad = store.getQuads(p, SH + 'path', null)[0]?.object;
|
||||
if (pathQuad) path = parsePath(store, pathQuad);
|
||||
return {
|
||||
path,
|
||||
constraints: readConstraints(store, p, { withLabel }),
|
||||
};
|
||||
}
|
||||
|
||||
function parseColumnList(store, listNode) {
|
||||
const columns = [];
|
||||
let cur = listNode;
|
||||
while (cur && cur.value !== RDF + 'nil') {
|
||||
const item = store.getQuads(cur, RDF + 'first', null)[0]?.object;
|
||||
if (item) columns.push(parseColumn(store, item));
|
||||
cur = store.getQuads(cur, RDF + 'rest', null)[0]?.object;
|
||||
if (!cur) throw new Error('rep:columns list is broken (no rdf:rest terminator)');
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
|
||||
function parseColumn(store, col) {
|
||||
let path = [];
|
||||
const pathQuad = store.getQuads(col, SH + 'path', null)[0]?.object;
|
||||
if (pathQuad) {
|
||||
if (pathQuad.termType === 'NamedNode' && pathQuad.value === REP + 'self') {
|
||||
path = []; // rep:self: the row's own subject
|
||||
} else {
|
||||
path = parsePath(store, pathQuad);
|
||||
}
|
||||
}
|
||||
return {
|
||||
path,
|
||||
label: store.getQuads(col, RDFS_LABEL, null)[0]?.object.value || null,
|
||||
collect: store.getQuads(col, REP + 'collect', null)[0]?.object.value || null,
|
||||
separator: store.getQuads(col, SH + 'separator', null)[0]?.object.value || null,
|
||||
constraints: readConstraints(store, col), // kept so check.js can refuse it
|
||||
};
|
||||
}
|
||||
|
||||
// A path is a single node (predicate, or a blank with sh:inversePath)
|
||||
// or an RDF list of predicates. Returns Hop[] = { predicate, inverse }.
|
||||
function parsePath(store, pathQuad) {
|
||||
if (pathQuad.termType === 'BlankNode') {
|
||||
const inv = store.getQuads(pathQuad, SH + 'inversePath', null)[0]?.object;
|
||||
if (inv) return [{ predicate: inv.value, inverse: true }];
|
||||
const first = store.getQuads(pathQuad, RDF + 'first', null)[0]?.object;
|
||||
if (!first) throw new Error('sh:path blank node without sh:inversePath is not supported');
|
||||
}
|
||||
if (pathQuad.termType === 'NamedNode') {
|
||||
return [{ predicate: pathQuad.value, inverse: false }];
|
||||
}
|
||||
// RDF list of predicates
|
||||
const hops = [];
|
||||
let cur = pathQuad;
|
||||
while (cur && cur.value !== RDF + 'nil') {
|
||||
const item = store.getQuads(cur, RDF + 'first', null)[0]?.object;
|
||||
if (!item) throw new Error('sh:path list is broken');
|
||||
if (item.termType !== 'NamedNode') throw new Error('sh:path list items must be URIs');
|
||||
hops.push({ predicate: item.value, inverse: false });
|
||||
cur = store.getQuads(cur, RDF + 'rest', null)[0]?.object;
|
||||
}
|
||||
return hops;
|
||||
}
|
||||
|
||||
function readConstraints(store, p, { withLabel = false } = {}) {
|
||||
const one = (pred) => store.getQuads(p, pred, null)[0]?.object || null;
|
||||
const constraints = {};
|
||||
const minCount = one(SH + 'minCount');
|
||||
if (minCount) constraints.minCount = Number(minCount.value);
|
||||
const maxCount = one(SH + 'maxCount');
|
||||
if (maxCount) constraints.maxCount = Number(maxCount.value);
|
||||
const hasValue = one(SH + 'hasValue');
|
||||
if (hasValue) constraints.hasValue = term(hasValue);
|
||||
const inList = one(SH + 'in');
|
||||
if (inList) constraints.in = readRdfList(store, inList).map(term);
|
||||
for (const [key, pred] of [
|
||||
['minInclusive', SH + 'minInclusive'], ['maxInclusive', SH + 'maxInclusive'],
|
||||
['minExclusive', SH + 'minExclusive'], ['maxExclusive', SH + 'maxExclusive'],
|
||||
]) {
|
||||
const q = one(pred);
|
||||
if (q) constraints[key] = term(q);
|
||||
}
|
||||
const pattern = one(SH + 'pattern');
|
||||
if (pattern) constraints.pattern = pattern.value;
|
||||
const flags = one(SH + 'flags');
|
||||
if (flags) constraints.flags = flags.value;
|
||||
const anyOf = one(REP + 'anyOf');
|
||||
if (anyOf) constraints.anyOf = readRdfList(store, anyOf).map(term)
|
||||
.map(t => ({ type: t.type, value: t.value }));
|
||||
// read but never used by the runner; check.js refuses it (a filter has no label)
|
||||
if (withLabel) {
|
||||
const label = one(RDFS_LABEL);
|
||||
if (label) constraints.label = label.value;
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
function readRdfList(store, node) {
|
||||
const items = [];
|
||||
let cur = node;
|
||||
while (cur && cur.value !== RDF + 'nil') {
|
||||
const item = store.getQuads(cur, RDF + 'first', null)[0]?.object;
|
||||
if (!item) throw new Error('RDF list is broken');
|
||||
items.push(item);
|
||||
cur = store.getQuads(cur, RDF + 'rest', null)[0]?.object;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function term(object) {
|
||||
if (object.termType === 'NamedNode') return { type: 'uri', value: object.value };
|
||||
return {
|
||||
type: 'literal',
|
||||
value: object.value,
|
||||
datatype: object.datatype ? object.datatype.value : null,
|
||||
language: object.language || null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user