| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 | /*	MIT License http://www.opensource.org/licenses/mit-license.php	Author Tobias Koppers @sokra*/"use strict";const { SyncHook } = require("tapable");/** @typedef {function(string): boolean} RuleConditionFunction *//** * @typedef {object} RuleCondition * @property {string | string[]} property * @property {boolean} matchWhenEmpty * @property {RuleConditionFunction} fn *//** * @typedef {object} Condition * @property {boolean} matchWhenEmpty * @property {RuleConditionFunction} 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			.filter(Boolean)			.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 === "") {			return {				matchWhenEmpty: true,				fn: str => str === ""			};		}		if (!condition) {			throw this.error(				path,				condition,				"Expected condition but got falsy value"			);		}		if (typeof condition === "string") {			return {				matchWhenEmpty: condition.length === 0,				fn: str => typeof str === "string" && 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 => typeof v === "string" && 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.or,								"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;
 |