@rmlio/yarrrml-parser
Advanced tools
Comparing version 1.4.0 to 1.5.0
@@ -17,2 +17,3 @@ #!/usr/bin/env node | ||
const namespaces = require('prefix-ns').asMap(); | ||
namespaces['rml'] = 'http://semweb.mmlab.be/ns/rml#'; // this one is the only official one for now. | ||
const watch = require('../lib/watcher.js'); | ||
@@ -19,0 +20,0 @@ const glob = require('glob'); |
@@ -10,2 +10,13 @@ # Changelog | ||
## [1.5.0] - 2023-06-13 | ||
### Added | ||
- Support for LDES | ||
### Fixed | ||
- expander: fix graphs example from spec (see [issue 188](https://github.com/RMLio/yarrrml-parser/issues/188)) | ||
- wrong namespace gets fetched from prefix.cc for rml prefix. Hardcode it. | ||
## [1.4.0] - 2022-11-18 | ||
@@ -280,2 +291,3 @@ | ||
[1.5.0]: https://github.com/RMLio/yarrrml-parser/compare/v1.4.0...v1.5.0 | ||
[1.4.0]: https://github.com/RMLio/yarrrml-parser/compare/v1.3.6...v1.4.0 | ||
@@ -282,0 +294,0 @@ [1.3.6]: https://github.com/RMLio/yarrrml-parser/compare/v1.3.5...v1.3.6 |
@@ -40,3 +40,3 @@ /** | ||
expandTargetsInDocument(output); | ||
rewriteLDESes(output); | ||
return output; | ||
@@ -56,5 +56,5 @@ } | ||
expandSourcesInMapping(mapping, mappingKey); | ||
expandTargetsInMapping(mapping, mappingKey); | ||
expandPredicateObjects(mapping, mappingKey); | ||
expandGraphs(mapping); | ||
expandTargetsInMapping(mapping); | ||
} else { | ||
@@ -75,2 +75,7 @@ Logger.warn(`mapping "${mappingKey}": no rules are provided. Skipping.`); | ||
if (typeof mapping.subjects === 'string') { | ||
if (mapping.subjects.endsWith('~ldes')) { | ||
mapping.subjects = mapping.subjects.slice(0, -5) | ||
Logger.debug('LDES annotation found'); | ||
mapping.subjects = expandLDES(mapping.subjects); | ||
} | ||
mapping.subjects = [mapping.subjects] | ||
@@ -80,2 +85,3 @@ } else if (Array.isArray(mapping.subjects)) { | ||
if (typeof mapping.subjects[i] === 'object') { | ||
mapping.subjects[i] = expandLDES(mapping.subjects[i]) | ||
expandFunction(mapping.subjects[i]); | ||
@@ -141,5 +147,3 @@ | ||
function expandTargetsInMapping(mapping, mappingKey) { | ||
let targets = []; | ||
function expandTargetsInMapping(mapping) { | ||
// Replace shortcuts | ||
@@ -150,3 +154,3 @@ replaceAll('targets', mapping); | ||
if (mapping.subjects) { | ||
mapping.subjects.forEach((subject) => { | ||
mapping.subjects.forEach(subject => { | ||
if (subject.targets) { | ||
@@ -410,3 +414,3 @@ if (Array.isArray(subject.targets)) { | ||
expandFunction(po.objects[j]); | ||
expandFunction(po.objects[j], true); | ||
@@ -423,3 +427,3 @@ //condition | ||
po.objects[j].conditions.forEach(c => { | ||
expandFunction(c); | ||
expandFunction(c, true); | ||
}); | ||
@@ -449,3 +453,3 @@ | ||
function expandFunction(input) { | ||
function expandFunction(input, canHaveObject = false) { | ||
replaceAll('function', input); | ||
@@ -486,7 +490,5 @@ replaceAll('parameters', input); | ||
if (e[2] === "subject" || e[2] === "object") { | ||
input.parameters[i].from = e[2]; | ||
input.parameters[i].from = canHaveObject ? e[2] : "subject"; | ||
} else { | ||
const e = new Error(`\`from\` has to have the value "s", "subject", "o", or "object`); | ||
e.code = 'INVALID_YAML'; | ||
throw e; | ||
Logger.error(`\`from\` has to have the value "s", "subject", "o", or "object`); | ||
} | ||
@@ -497,3 +499,3 @@ } else { | ||
} else if (e[0] === 'str2') { | ||
input.parameters[i].from = "object" | ||
input.parameters[i].from = canHaveObject ? "object" : "subject"; | ||
} else { | ||
@@ -511,3 +513,3 @@ // I know this can be written shorter, but makes for a bit more clearer code | ||
if (e.value instanceof Object) { | ||
expandFunction(e.value); | ||
expandFunction(e.value, canHaveObject); | ||
e.from = 'function'; | ||
@@ -737,2 +739,262 @@ } | ||
function expandLDES(subject) { | ||
let ldes = {}; | ||
if (subject !== undefined) { | ||
if (typeof subject === 'string') { | ||
ldes.memberSubject = subject; | ||
subject = { | ||
value: subject, | ||
}; | ||
} else if (typeof subject === 'object' && subject !== null) { | ||
if (subject.ldes !== undefined && subject.ldes !== null) { | ||
ldes = subject.ldes; | ||
ldes.memberSubject = subject.value | ||
} else { | ||
// there is no LDES | ||
return subject; | ||
} | ||
} | ||
} else { | ||
// no subject, no LDES | ||
return subject; | ||
} | ||
// the 'shape' key is just copied... | ||
if (ldes.id === undefined || ldes.id === null) { | ||
ldes.id = 'http://example.org/eventstream'; | ||
} | ||
// TODO: watchedProperties: allow shortcut watchedProperties: <property str> ? | ||
if (ldes.watchedProperties === undefined) { | ||
ldes.watchedProperties = []; | ||
} else { | ||
// just check if it's an array | ||
if (!Array.isArray(ldes.watchedProperties)) { | ||
Logger.error('LDES `watchedProperties` must be an array.'); | ||
} | ||
} | ||
// TODO: versionOfPath: allow shortcut versionOfPath: <predicate> ? | ||
if (ldes.versionOfPath !== undefined && ldes.versionOfPath !== null) { | ||
let newVersionOfPath = {}; | ||
if (Array.isArray(ldes.versionOfPath)) { | ||
if (ldes.versionOfPath.length === 0) { | ||
newVersionOfPath.predicate = 'http://purl.org/dc/terms/isVersionOf'; | ||
newVersionOfPath.object = null; | ||
} else if (ldes.versionOfPath.length === 1) { | ||
newVersionOfPath.predicate = ldes.versionOfPath[0]; | ||
newVersionOfPath.object = null; | ||
} else if (ldes.versionOfPath.length === 2) { | ||
newVersionOfPath.predicate = ldes.versionOfPath[0]; | ||
newVersionOfPath.object = ldes.versionOfPath[1]; | ||
if (!newVersionOfPath.object.endsWith('~iri')) { | ||
newVersionOfPath.object += '~iri'; | ||
} | ||
} else { | ||
Logger.error('LDES `versionOfPath` must be an array of maximum two strings.'); | ||
} | ||
} else { | ||
Logger.error('LDES `versionOfPath` must be an array.'); | ||
} | ||
const expandedpos = expandPredicateAndObject(newVersionOfPath.predicate, newVersionOfPath.object); | ||
newVersionOfPath.predicate = expandedpos[0]; | ||
newVersionOfPath.object = expandedpos[1]; | ||
ldes.versionOfPath = newVersionOfPath; | ||
} else { | ||
ldes.versionOfPath = null; | ||
} | ||
// TODO: timestampPath: allow shortcut timestampPath: <predicate> ? | ||
if (ldes.timestampPath !== undefined && ldes.timestampPath !== null) { | ||
let newTimestampPath = {}; | ||
if (Array.isArray(ldes.timestampPath)) { | ||
if (ldes.timestampPath.length >= 1) { | ||
newTimestampPath.predicate = ldes.timestampPath[0]; | ||
newTimestampPath.object = null; | ||
newTimestampPath.oDataType = null; | ||
// TODO the predicate and object have to be present as predicateobject mapping; check later? | ||
if (ldes.timestampPath.length >= 2) { | ||
newTimestampPath.object = ldes.timestampPath[1]; | ||
// TODO: add predicateobjectmapping if not exists | ||
if (ldes.timestampPath.length === 3) { | ||
newTimestampPath.oDataType = ldes.timestampPath[2]; | ||
} else { | ||
Logger.error('LDES `timestampPath` must be an array of maximum three strings.'); | ||
} | ||
} | ||
} else { | ||
Logger.error('LDES `timestampPath` must be an array of minimum one string.'); | ||
} | ||
} else { | ||
Logger.error('LDES timestampPath must be an array.'); | ||
} | ||
const expandedpos = expandPredicateAndObject(newTimestampPath.predicate, newTimestampPath.object); | ||
newTimestampPath.predicate = expandedpos[0]; | ||
newTimestampPath.object = expandedpos[1]; | ||
ldes.timestampPath = newTimestampPath; | ||
} else { | ||
ldes.timestampPath = null; | ||
} | ||
if (ldes.memberIDFunction !== undefined && ldes.memberIDFunction !== null) { | ||
expandFunction(ldes.memberIDFunction); | ||
} else { | ||
// use the default generateUniqueIRI function, so prepare parameters | ||
const watchedPropertiesStr = _toWatchedPropertiesString(ldes.watchedProperties); | ||
ldes.memberIDFunction = { | ||
function: idlabfn + 'generateUniqueIRI', | ||
parameters: [ | ||
{ | ||
parameter: idlabfn + 'iri', | ||
value: ldes.memberSubject, | ||
from: 'subject', | ||
type: 'iri', | ||
}, | ||
{ | ||
parameter: idlabfn + 'watchedProperty', | ||
value: watchedPropertiesStr, | ||
from: 'subject', | ||
type: 'literal' | ||
} | ||
], | ||
}; | ||
} | ||
if (ldes.shape === undefined) { | ||
ldes.shape = null; | ||
} | ||
subject.ldes = ldes; | ||
return subject; | ||
} | ||
/** | ||
* This rewrites the mappings so that LDES specific triples can be generated | ||
* according to the CURRENT implementation in RMLMapper. If that implementation changes, | ||
* and it will, then this function definitely needs to change. | ||
* (see https://dylanvanassche.be/assets/pdf/eswc2022-rml-ldes.pdf) | ||
* @param document the YARRRML document to rewrite LDES stuff for. | ||
*/ | ||
function rewriteLDESes(document) { | ||
if (document.mappings !== undefined && document.mappings !== null) { | ||
for (let mapping of Object.values(document.mappings)) { | ||
for (let subject of mapping.subjects) { | ||
if (subject.ldes !== null && subject.ldes !== undefined) { | ||
const ldes = subject.ldes; | ||
if (subject.targets !== null && subject.targets !== undefined) { | ||
for (let target of subject.targets) { | ||
// if the target is of type string, then it's a reference to a target definition | ||
let realTarget = typeof target === 'string' ? document.targets[target] : target; | ||
// make the target an LDES target! | ||
realTarget.ldes = { | ||
id: ldes.id, | ||
versionOfPath: ldes.versionOfPath, | ||
timestampPath: ldes.timestampPath, | ||
shape: ldes.shape | ||
}; | ||
} | ||
// check / add LDES-related predicate-object mappings for versionOfPath | ||
if (ldes.versionOfPath !== null) { | ||
// check if predicate & object mappings exist. If not, add them. | ||
addLDESSpecificPredicateObjects(ldes.versionOfPath.predicate, ldes.versionOfPath.object, mapping.predicateobjects); | ||
} | ||
// check / add LDES-related predicate-object mappings for timestampPath | ||
if (ldes.timestampPath !== null) { | ||
// check if predicate & object mappings exist. If not, add them. | ||
addLDESSpecificPredicateObjects(ldes.timestampPath.predicate, ldes.timestampPath.object, mapping.predicateobjects); | ||
} | ||
// remove ldes properties object from subject | ||
delete subject.ldes; | ||
// replace the subject with the LDES member id function | ||
subject.function = ldes.memberIDFunction.function; | ||
subject.parameters = ldes.memberIDFunction.parameters; | ||
delete subject.value; | ||
} else { // If there are no targets, we have to add an LDES target! | ||
// TODO: for the current implementation of LDES in RML, there HAS to be a target. | ||
// This will change in the upcoming new implementation | ||
Logger.error('The current RML LDES generation mappings require a target at subject mapping level. ' + | ||
'This will change in the future.'); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
function addLDESSpecificPredicateObjects(predicate, object, predicateobjects) { | ||
let found = false; | ||
for (let predicateobject of predicateobjects) { | ||
for (let i = 0; i < predicateobject.predicates.length; i++) { | ||
// if the predicate exists, the object must be the same if the given object is not null | ||
if (predicateobject.predicates[i] === predicate) { | ||
found = true; | ||
if (object !== null && predicateobject.objects[i] !== object) { | ||
Logger.error(`The versionOf or timestampPath predicate ${predicate} expects an object mapping ${object}, but is ${predicateobject.objects[i]}`); | ||
} | ||
} | ||
} | ||
} | ||
if (!found) { | ||
predicateobjects.push({ | ||
predicates: [predicate], | ||
objects: [object] | ||
}); | ||
} | ||
} | ||
// helper function to put LDES watched properties into one string to pass to generateUniqueIRI function | ||
function _toWatchedPropertiesString(watchedPropertiesArray) { | ||
let resultStr = ''; | ||
if (watchedPropertiesArray.length > 0) { | ||
const watchedProperty = _parseTemplate(watchedPropertiesArray[0]); | ||
resultStr = watchedProperty.concat('=$(').concat(watchedProperty).concat(')'); | ||
} | ||
for (let i = 1; i < watchedPropertiesArray.length; i++) { | ||
const watchedProperty = _parseTemplate(watchedPropertiesArray[i]); | ||
const wpStr = '&'.concat(watchedProperty).concat('=$(').concat(watchedProperty).concat(')'); | ||
resultStr = resultStr.concat(wpStr); | ||
} | ||
return resultStr; | ||
} | ||
// TODO: Almost literally copied from abstract-generator.js. Is there an elegant way of re-using that function? | ||
function _parseTemplate(t) { | ||
t = '' + t; // Make sure it's a string. | ||
t = t.replace(/\\\\/g, '@@@BACKWARD-SLASH@@@'); // We want to preserve real backward slashes. | ||
t = t.replace(/\\\(/g, '@@@BRACKET-OPEN@@@'); // Same for opening brackets. | ||
t = t.replace(/\\\)/g, '@@@BRACKET-CLOSE@@@'); // Same for closing brackets. | ||
t = t.replace(/\$\(([^)]*)\)/g, "$1"); | ||
t = t.replace(/@@@BRACKET-CLOSE@@@/g, ')'); | ||
t = t.replace(/@@@BRACKET-OPEN@@@/g, '('); | ||
t = t.replace(/@@@BACKWARD-SLASH@@@/g, '/'); | ||
return t; | ||
} | ||
function expandPredicateAndObject(predicate, object) { | ||
const predicateobjects = [{ | ||
predicates: [predicate], | ||
objects: [object] | ||
}]; | ||
expandPredicates(predicateobjects); | ||
if (object !== null) { | ||
expandObjects(predicateobjects); | ||
return [predicateobjects[0].predicates[0], predicateobjects[0].objects[0]]; | ||
} else { | ||
return [predicateobjects[0].predicates[0], null]; | ||
} | ||
} | ||
module.exports = expand; |
@@ -23,2 +23,4 @@ /** | ||
namespaces.comp = 'http://semweb.mmlab.be/ns/rml-compression#'; | ||
namespaces.ldes = 'https://w3id.org/ldes#'; | ||
namespaces.tree = 'https://w3id.org/tree#'; | ||
@@ -182,3 +184,7 @@ class RMLGenerator extends AbstractGenerator { | ||
Object.keys(target).forEach(key => { | ||
id += target[key] | ||
if (key === 'ldes') { | ||
id += 'ldes'; | ||
} else { | ||
id += target[key]; | ||
} | ||
id += '-' | ||
@@ -475,9 +481,9 @@ }); | ||
if (targetName) { | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.rdfs + 'label'), | ||
literal(targetName) | ||
)); | ||
} | ||
if (targetName) { | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.rdfs + 'label'), | ||
literal(targetName) | ||
)); | ||
} | ||
@@ -707,2 +713,50 @@ /* Serialization format */ | ||
// Add LDES specific triples, if any | ||
if (target.ldes) { | ||
this.prefixes['ldes'] = namespaces.ldes; | ||
this.prefixes['tree'] = namespaces.tree; | ||
// say it's an LDES logical target | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.rdf + 'type'), | ||
namedNode(namespaces.ldes + 'EventStreamTarget'), | ||
)); | ||
// add the required LDES "base" iri | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.ldes + 'baseIRI'), | ||
namedNode(target.ldes.id), | ||
)); | ||
// Add the SHACL shape. While, according to the LDES spec it is optional, | ||
// the current RMLMapper implementation requires it. | ||
const shaclshape = target.ldes.shape ? target.ldes.shape : 'http://example.org/shape.shacl'; | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.tree + 'shape'), | ||
namedNode(shaclshape), | ||
)); | ||
// add the timestampPath | ||
if (target.ldes.timestampPath) { | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.ldes + 'timestampPath'), | ||
namedNode(target.ldes.timestampPath.predicate), | ||
)); | ||
} | ||
// add the versionOfPath | ||
if (target.ldes.versionOfPath) { | ||
this.quads.push(quad( | ||
tSubject, | ||
namedNode(namespaces.ldes + 'versionOfPath'), | ||
namedNode(target.ldes.versionOfPath.predicate), | ||
)); | ||
} | ||
} | ||
return tSubject; | ||
@@ -709,0 +763,0 @@ } |
@@ -19,2 +19,5 @@ /** | ||
doTestCase('spec/mapping-with-database-as-source', options); | ||
doTestCase('spec/graphs-all-triples', options); | ||
}); | ||
@@ -396,2 +399,12 @@ | ||
}); | ||
describe('LDES', () => { | ||
it('works for watched properties', function (done) { | ||
work('ldes/watched-properties-only/mapping.yaml', 'ldes/watched-properties-only/mapping.rml.ttl', done, {includeMetadata: false}); | ||
}); | ||
it('works for a custom LDES ID', function (done) { | ||
work('ldes/custom-ldes-id/mapping.yaml', 'ldes/custom-ldes-id/mapping.rml.ttl', done, {includeMetadata: false}); | ||
}); | ||
}); | ||
}); | ||
@@ -398,0 +411,0 @@ |
@@ -55,3 +55,3 @@ const assert = require('assert'); | ||
} catch (e) { | ||
assert.deepStrictEqual(yamlQuads.map(q => q.toString()).sort(), rmlQuads.map(q => q.toString()).sort()); | ||
assert.equal(yamlQuads.map(q => quadToBadString(q)).sort().join('\n'), rmlQuads.map(q => quadToBadString(q)).sort().join('\n')); | ||
} | ||
@@ -64,2 +64,6 @@ cb(); | ||
function quadToBadString(quad) { | ||
return `<${quad.subject.value}> <${quad.predicate.value}> <${quad.object.value}>` | ||
} | ||
function compareY2R2RData(yaml, ttl, options, cb) { | ||
@@ -66,0 +70,0 @@ const y2r = new convertYAMLtoR2RML(options); |
{ | ||
"name": "@rmlio/yarrrml-parser", | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"description": "Parse YARRRML descriptions into RML RDF statements", | ||
@@ -5,0 +5,0 @@ "main": "lib/yarrrml2rml.js", |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
770075
309
6014