Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F117875649
RuleSetCompiler.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Mon, Jun 16, 08:54
Size
8 KB
Mime Type
text/x-c++
Expires
Wed, Jun 18, 08:54 (2 d)
Engine
blob
Format
Raw Data
Handle
26803562
Attached To
rOACCT Open Access Compliance Check Tool (OACCT)
RuleSetCompiler.js
View Options
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const { SyncHook } = require("tapable");
/**
* @typedef {Object} RuleCondition
* @property {string | string[]} property
* @property {boolean} matchWhenEmpty
* @property {function(string): boolean} fn
*/
/**
* @typedef {Object} Condition
* @property {boolean} matchWhenEmpty
* @property {function(string): boolean} fn
*/
/**
* @typedef {Object} CompiledRule
* @property {RuleCondition[]} conditions
* @property {(Effect|function(object): Effect[])[]} effects
* @property {CompiledRule[]=} rules
* @property {CompiledRule[]=} oneOf
*/
/**
* @typedef {Object} Effect
* @property {string} type
* @property {any} value
*/
/**
* @typedef {Object} RuleSet
* @property {Map<string, any>} references map of references in the rule set (may grow over time)
* @property {function(object): Effect[]} exec execute the rule set
*/
class RuleSetCompiler {
constructor(plugins) {
this.hooks = Object.freeze({
/** @type {SyncHook<[string, object, Set<string>, CompiledRule, Map<string, any>]>} */
rule: new SyncHook([
"path",
"rule",
"unhandledProperties",
"compiledRule",
"references"
])
});
if (plugins) {
for (const plugin of plugins) {
plugin.apply(this);
}
}
}
/**
* @param {object[]} ruleSet raw user provided rules
* @returns {RuleSet} compiled RuleSet
*/
compile(ruleSet) {
const refs = new Map();
const rules = this.compileRules("ruleSet", ruleSet, refs);
/**
* @param {object} data data passed in
* @param {CompiledRule} rule the compiled rule
* @param {Effect[]} effects an array where effects are pushed to
* @returns {boolean} true, if the rule has matched
*/
const execRule = (data, rule, effects) => {
for (const condition of rule.conditions) {
const p = condition.property;
if (Array.isArray(p)) {
let current = data;
for (const subProperty of p) {
if (
current &&
typeof current === "object" &&
Object.prototype.hasOwnProperty.call(current, subProperty)
) {
current = current[subProperty];
} else {
current = undefined;
break;
}
}
if (current !== undefined) {
if (!condition.fn(current)) return false;
continue;
}
} else if (p in data) {
const value = data[p];
if (value !== undefined) {
if (!condition.fn(value)) return false;
continue;
}
}
if (!condition.matchWhenEmpty) {
return false;
}
}
for (const effect of rule.effects) {
if (typeof effect === "function") {
const returnedEffects = effect(data);
for (const effect of returnedEffects) {
effects.push(effect);
}
} else {
effects.push(effect);
}
}
if (rule.rules) {
for (const childRule of rule.rules) {
execRule(data, childRule, effects);
}
}
if (rule.oneOf) {
for (const childRule of rule.oneOf) {
if (execRule(data, childRule, effects)) {
break;
}
}
}
return true;
};
return {
references: refs,
exec: data => {
/** @type {Effect[]} */
const effects = [];
for (const rule of rules) {
execRule(data, rule, effects);
}
return effects;
}
};
}
/**
* @param {string} path current path
* @param {object[]} rules the raw rules provided by user
* @param {Map<string, any>} refs references
* @returns {CompiledRule[]} rules
*/
compileRules(path, rules, refs) {
return rules.map((rule, i) =>
this.compileRule(`${path}[${i}]`, rule, refs)
);
}
/**
* @param {string} path current path
* @param {object} rule the raw rule provided by user
* @param {Map<string, any>} refs references
* @returns {CompiledRule} normalized and compiled rule for processing
*/
compileRule(path, rule, refs) {
const unhandledProperties = new Set(
Object.keys(rule).filter(key => rule[key] !== undefined)
);
/** @type {CompiledRule} */
const compiledRule = {
conditions: [],
effects: [],
rules: undefined,
oneOf: undefined
};
this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
if (unhandledProperties.has("rules")) {
unhandledProperties.delete("rules");
const rules = rule.rules;
if (!Array.isArray(rules))
throw this.error(path, rules, "Rule.rules must be an array of rules");
compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
}
if (unhandledProperties.has("oneOf")) {
unhandledProperties.delete("oneOf");
const oneOf = rule.oneOf;
if (!Array.isArray(oneOf))
throw this.error(path, oneOf, "Rule.oneOf must be an array of rules");
compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
}
if (unhandledProperties.size > 0) {
throw this.error(
path,
rule,
`Properties ${Array.from(unhandledProperties).join(", ")} are unknown`
);
}
return compiledRule;
}
/**
* @param {string} path current path
* @param {any} condition user provided condition value
* @returns {Condition} compiled condition
*/
compileCondition(path, condition) {
if (!condition) {
throw this.error(
path,
condition,
"Expected condition but got falsy value"
);
}
if (typeof condition === "string") {
return {
matchWhenEmpty: condition.length === 0,
fn: str => str.startsWith(condition)
};
}
if (typeof condition === "function") {
try {
return {
matchWhenEmpty: condition(""),
fn: condition
};
} catch (err) {
throw this.error(
path,
condition,
"Evaluation of condition function threw error"
);
}
}
if (condition instanceof RegExp) {
return {
matchWhenEmpty: condition.test(""),
fn: v => condition.test(v)
};
}
if (Array.isArray(condition)) {
const items = condition.map((c, i) =>
this.compileCondition(`${path}[${i}]`, c)
);
return this.combineConditionsOr(items);
}
if (typeof condition !== "object") {
throw this.error(
path,
condition,
`Unexpected ${typeof condition} when condition was expected`
);
}
const conditions = [];
for (const key of Object.keys(condition)) {
const value = condition[key];
switch (key) {
case "or":
if (value) {
if (!Array.isArray(value)) {
throw this.error(
`${path}.or`,
condition.and,
"Expected array of conditions"
);
}
conditions.push(this.compileCondition(`${path}.or`, value));
}
break;
case "and":
if (value) {
if (!Array.isArray(value)) {
throw this.error(
`${path}.and`,
condition.and,
"Expected array of conditions"
);
}
let i = 0;
for (const item of value) {
conditions.push(this.compileCondition(`${path}.and[${i}]`, item));
i++;
}
}
break;
case "not":
if (value) {
const matcher = this.compileCondition(`${path}.not`, value);
const fn = matcher.fn;
conditions.push({
matchWhenEmpty: !matcher.matchWhenEmpty,
fn: v => !fn(v)
});
}
break;
default:
throw this.error(
`${path}.${key}`,
condition[key],
`Unexpected property ${key} in condition`
);
}
}
if (conditions.length === 0) {
throw this.error(
path,
condition,
"Expected condition, but got empty thing"
);
}
return this.combineConditionsAnd(conditions);
}
/**
* @param {Condition[]} conditions some conditions
* @returns {Condition} merged condition
*/
combineConditionsOr(conditions) {
if (conditions.length === 0) {
return {
matchWhenEmpty: false,
fn: () => false
};
} else if (conditions.length === 1) {
return conditions[0];
} else {
return {
matchWhenEmpty: conditions.some(c => c.matchWhenEmpty),
fn: v => conditions.some(c => c.fn(v))
};
}
}
/**
* @param {Condition[]} conditions some conditions
* @returns {Condition} merged condition
*/
combineConditionsAnd(conditions) {
if (conditions.length === 0) {
return {
matchWhenEmpty: false,
fn: () => false
};
} else if (conditions.length === 1) {
return conditions[0];
} else {
return {
matchWhenEmpty: conditions.every(c => c.matchWhenEmpty),
fn: v => conditions.every(c => c.fn(v))
};
}
}
/**
* @param {string} path current path
* @param {any} value value at the error location
* @param {string} message message explaining the problem
* @returns {Error} an error object
*/
error(path, value, message) {
return new Error(
`Compiling RuleSet failed: ${message} (at ${path}: ${value})`
);
}
}
module.exports = RuleSetCompiler;
Event Timeline
Log In to Comment