153 lines
5.7 KiB
JavaScript
153 lines
5.7 KiB
JavaScript
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,
|
|
};
|
|
} |