UNPKG

@shexjs/util

Version:

Shape Expressions validation and utilities.

247 lines (221 loc) 9.21 kB
const ShExUtil = require('../shex-util'); const {ShExIndexVisitor} = require('@shexjs/visitor'); /** * * Collision API: * @param type element of schema: imports|start|startActs|_locations... * @param left structure with duplicated item. * @param right structure with introducing duplicate item. * @param leftLloc? yylloc structure for source of left item * @param rightLloc? ylloc structure for source of right item * @returns {boolean} false: keep left, true: overwrite with right. May also throw. * */ class Merger { /** * Join to ShExJ schemas. The schemas may have `_index` and `_locations` attributes. * @param left first schema to be joined. * @param right second schema to be joined. * @param collision the string "left" or "right" or a function folling the collision API. * @param inPlace if true, edit the left schema directly. * @returns ShExJ schema */ constructor (olde, ...args) { this.left = olde.schema; this.leftMeta = olde.schemaMeta let newe, collision = 'throw', inPlace = false; switch (args.length) { case 0: break; case 1: collision = args[0]; break; case 2: collision = args[0]; inPlace = args[1]; break; case 3: this.right = args[0].schema; this.rightMeta = args[0].schemaMeta collision = args[0]; inPlace = args[1]; break; default: throw Error(`Did not expect ${args.length} arguments to Merger`); } this.overwrite = collision === 'left' ? () => false : collision === 'right' ? () => true : typeof collision === 'object' ? (type, left, right, leftLloc, rightLloc) => collision.overwrite(type, left, right, leftLloc, rightLloc, this.leftMeta, this.rightMeta) : typeof collision === 'function' ? collision : (type, left, right, _leftLloc, _rightLloc, _leftMeta, _rightMeta) => { throw Error(`${type} ${JSON.stringify(right, null, 2)} collides with ${JSON.stringify(left, null, 2)}`); }; this.inPlace = inPlace; this.ret = inPlace ? this.left : ShExUtil.emptySchema(); } mergeArray (attr) { Object.keys(this.left[attr] || {}).forEach(key => { if (!(attr in this.ret)) this.ret[attr] = {}; this.ret[attr][key] = this.left[attr][key]; }); Object.keys(this.right[attr] || {}).forEach(key => { if (!(attr in this.left) || !(key in this.left[attr]) || (this.left[attr][key] !== this.right[attr][key] && this.overwrite(attr, this.ret[attr][key], this.right[attr][key], undefined, undefined, this.leftMeta, this.rightMeta))) { if (!(attr in this.ret)) this.ret[attr] = {}; this.ret[attr][key] = this.right[attr][key]; } }); } mergeMap (attr, myOverwrite = this.overwrite) { (this.left[attr] || new Map()).forEach((value, key, map) => { if (!(attr in this.ret)) this.ret[attr] = new Map(); this.ret[attr].set(key, this.left[attr].get(key)); }); (this.right[attr] || new Map()).forEach((value, key, map) => { if (!(attr in this.left) || !(this.left[attr].has(key)) || myOverwrite(attr, this.ret[attr].get(key), this.right[attr].get(key)), undefined, undefined, this.leftMeta, this.rightMeta) { if (!(attr in this.ret)) this.ret[attr] = new Map(); this.ret[attr].set(key, this.right[attr].get(key)); } }); } merge (...args) { switch (args.length) { case 0: if (!this.left) throw Error(`expected left argument to merge`); if (!this.right) throw Error(`expected right argument to merge`); break; case 1: this.right = args[0].schema; this.rightMeta = args[0].schemaMeta break; case 2: this.left = args[0].schema; this.leftMeta = args[0].schemaMeta this.right = args[1].schema; this.rightMeta = args[1].schemaMeta break; default: throw Error(`Did not expect ${args.length} arguments to merge`); } // base if ("_base" in this.left) this.ret._base = this.left._base; if ("_base" in this.right) if (!("_base" in this.left)/* || this.overwrite('_base', this.ret._base, this.right._base)*/) // _base favors the this.left this.ret._base = this.right._base; this.mergeArray("_prefixes"); this.mergeMap("_sourceMap", () => false); if ("_locations" in this.left || "_locations" in this.right) this.ret._locations = this.left._locations || {}; if ("imports" in this.right) if (!("imports" in this.left)) { this.ret.imports = this.right.imports; } else { [].push.apply(this.ret.imports, this.right.imports.filter( mprt => this.ret.imports.indexOf(mprt) === -1 )) } // startActs if ("startActs" in this.left) this.ret.startActs = this.left.startActs; if ("startActs" in this.right) if (!("startActs" in this.left) || this.overwrite('startActs', this.ret.startActs, this.right.startActs, undefined, undefined, this.leftMeta, this.rightMeta)) this.ret.startActs = this.right.startActs; // start if ("start" in this.left) this.ret.start = this.left.start; if ("start" in this.right) if (!("start" in this.left) || this.overwrite('start', this.ret.start, this.right.start, undefined, undefined, this.leftMeta, this.rightMeta)) this.ret.start = this.right.start; const lindex = this.left._index || ShExIndexVisitor.index(this.left); // shapes if (!this.inPlace) (this.left.shapes || []).forEach(lshape => { if (!("shapes" in this.ret)) this.ret.shapes = []; this.ret.shapes.push(lshape); }); (this.right.shapes || []).forEach(rshape => { if (!("shapes" in this.ret)) { this.ret.shapes = []; this.ret.shapes.push(rshape) lindex.shapeExprs[rshape.id] = rshape; } else { const previousDecl = lindex.shapeExprs[rshape.id]; if (!previousDecl) { this.ret.shapes.push(rshape) lindex.shapeExprs[rshape.id] = rshape; } else if (this.overwrite('shapeDecl', previousDecl, rshape, (this.left._locations || {})[rshape.id], (this.right._locations || {})[rshape.id], this.leftMeta, this.rightMeta)) { this.ret.shapes.splice(this.ret.shapes.indexOf(previousDecl), 1, rshape); lindex.shapeExprs[rshape.id] = rshape; } } if ("_locations" in this.ret) this.ret._locations[rshape.id] = (this.right._locations || {})[rshape.id]; }); if (this.left._index || this.right._index) this.ret._index = ShExIndexVisitor.index(this.ret); // inefficient; could build above return this.ret; } /** * A merge function collision handler that warns on duplicates and throws on redefinitions. * @param type element of schema: imports|start|startActs|_locations... * @param left structure with duplicated item. * @param right structure with introducing duplicate item. * @param leftLloc? yylloc structure for source of left item * @param rightLloc? ylloc structure for source of right item * @returns {boolean} false: keep left, true: overwrite with right. May also throw. */ static warnDuplicates (type, left, right, leftLloc, rightLloc, _leftMeta, _rightMeta) { if (type === "_prefixes") return false; if (type !== "shapeDecl") throw Error(`Unexpected ${type} conflict: ${JSON.stringify(left)}, ${JSON.stringify(right)}`); const lStr = JSON.stringify(left); const rStr = JSON.stringify(right); const wheresStr = []; if (leftLloc) wheresStr.push(yyllocToString(leftLloc)); if (rightLloc) wheresStr.push(yyllocToString(rightLloc)); if (lStr === rStr) { console.warn(`Duplicate definitions for ${left.id}: ${wheresStr.map(s => "\n " + s)}`) return false; // keep left/old assignment } throw new Error(`Conflicing definitions for ${left.id}:\n${locIndent(leftLloc)} ${lStr}\n${locIndent(rightLloc)} ${rStr}`); function locIndent (yylloc) { return yylloc ? " " + yyllocToString(yylloc) + ":\n" : ""; } } } function yyllocToString (yylloc) { return `${yylloc.filename}(${yylloc.first_line}:${yylloc.first_column}-${yylloc.last_line}:${yylloc.last_column})${yylloc.importers ? yylloc.importers.reverse().map(i => "\n <= " + i).join() : ""}` } /** * A merge function collision handler that accumulates redeclarations. */ class StoreDuplicates { constructor () { this.duplicates = {}; } overwrite (type, left, right, leftLloc, rightLloc, leftMeta, rightMeta) { if (type === "_prefixes") return false; if (type !== "shapeDecl") throw Error(`Unexpected ${type} conflict: ${JSON.stringify(left)}, ${JSON.stringify(right)}`); const id = left.id; if (!this.duplicates[id]) this.duplicates[id] = [{...leftLloc, importers: leftMeta.importers}]; this.duplicates[id].push({...rightLloc, importers: rightMeta.importers}) return false; // keep left/old assignment } } module.exports = {Merger, StoreDuplicates, yyllocToString};