/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ /* <rules>: <rule> <rules>: [<rule>] <rule>: { resource: { test: <condition>, include: <condition>, exclude: <condition>, }, resource: <condition>, -> resource.test test: <condition>, -> resource.test include: <condition>, -> resource.include exclude: <condition>, -> resource.exclude resourceQuery: <condition>, compiler: <condition>, issuer: <condition>, use: "loader", -> use[0].loader loader: <>, -> use[0].loader loaders: <>, -> use options: {}, -> use[0].options, query: {}, -> options parser: {}, use: [ "loader" -> use[x].loader ], use: [ { loader: "loader", options: {} } ], rules: [ <rule> ], oneOf: [ <rule> ] } <condition>: /regExp/ <condition>: function(arg) {} <condition>: "starting" <condition>: [<condition>] // or <condition>: { and: [<condition>] } <condition>: { or: [<condition>] } <condition>: { not: [<condition>] } <condition>: { test: <condition>, include: <condition>, exclude: <condition> } normalized: { resource: function(), resourceQuery: function(), compiler: function(), issuer: function(), use: [ { loader: string, options: string, <any>: <any> } ], rules: [<rule>], oneOf: [<rule>], <any>: <any>, } */ "use strict"; const notMatcher = matcher => { return str => { return !matcher(str); }; }; const orMatcher = items => { return str => { for (let i = 0; i < items.length; i++) { if (items[i](str)) return true; } return false; }; }; const andMatcher = items => { return str => { for (let i = 0; i < items.length; i++) { if (!items[i](str)) return false; } return true; }; }; module.exports = class RuleSet { constructor(rules) { this.references = Object.create(null); this.rules = RuleSet.normalizeRules(rules, this.references, "ref-"); } static normalizeRules(rules, refs, ident) { if (Array.isArray(rules)) { return rules.map((rule, idx) => { return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`); }); } else if (rules) { return [RuleSet.normalizeRule(rules, refs, ident)]; } else { return []; } } static normalizeRule(rule, refs, ident) { if (typeof rule === "string") { return { use: [ { loader: rule } ] }; } if (!rule) { throw new Error("Unexcepted null when object was expected as rule"); } if (typeof rule !== "object") { throw new Error( "Unexcepted " + typeof rule + " when object was expected as rule (" + rule + ")" ); } const newRule = {}; let useSource; let resourceSource; let condition; const checkUseSource = newSource => { if (useSource && useSource !== newSource) { throw new Error( RuleSet.buildErrorMessage( rule, new Error( "Rule can only have one result source (provided " + newSource + " and " + useSource + ")" ) ) ); } useSource = newSource; }; const checkResourceSource = newSource => { if (resourceSource && resourceSource !== newSource) { throw new Error( RuleSet.buildErrorMessage( rule, new Error( "Rule can only have one resource source (provided " + newSource + " and " + resourceSource + ")" ) ) ); } resourceSource = newSource; }; if (rule.test || rule.include || rule.exclude) { checkResourceSource("test + include + exclude"); condition = { test: rule.test, include: rule.include, exclude: rule.exclude }; try { newRule.resource = RuleSet.normalizeCondition(condition); } catch (error) { throw new Error(RuleSet.buildErrorMessage(condition, error)); } } if (rule.resource) { checkResourceSource("resource"); try { newRule.resource = RuleSet.normalizeCondition(rule.resource); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.resource, error)); } } if (rule.realResource) { try { newRule.realResource = RuleSet.normalizeCondition(rule.realResource); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.realResource, error)); } } if (rule.resourceQuery) { try { newRule.resourceQuery = RuleSet.normalizeCondition(rule.resourceQuery); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.resourceQuery, error)); } } if (rule.compiler) { try { newRule.compiler = RuleSet.normalizeCondition(rule.compiler); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.compiler, error)); } } if (rule.issuer) { try { newRule.issuer = RuleSet.normalizeCondition(rule.issuer); } catch (error) { throw new Error(RuleSet.buildErrorMessage(rule.issuer, error)); } } if (rule.loader && rule.loaders) { throw new Error( RuleSet.buildErrorMessage( rule, new Error( "Provided loader and loaders for rule (use only one of them)" ) ) ); } const loader = rule.loaders || rule.loader; if (typeof loader === "string" && !rule.options && !rule.query) { checkUseSource("loader"); newRule.use = RuleSet.normalizeUse(loader.split("!"), ident); } else if (typeof loader === "string" && (rule.options || rule.query)) { checkUseSource("loader + options/query"); newRule.use = RuleSet.normalizeUse( { loader: loader, options: rule.options, query: rule.query }, ident ); } else if (loader && (rule.options || rule.query)) { throw new Error( RuleSet.buildErrorMessage( rule, new Error( "options/query cannot be used with loaders (use options for each array item)" ) ) ); } else if (loader) { checkUseSource("loaders"); newRule.use = RuleSet.normalizeUse(loader, ident); } else if (rule.options || rule.query) { throw new Error( RuleSet.buildErrorMessage( rule, new Error( "options/query provided without loader (use loader + options)" ) ) ); } if (rule.use) { checkUseSource("use"); newRule.use = RuleSet.normalizeUse(rule.use, ident); } if (rule.rules) { newRule.rules = RuleSet.normalizeRules( rule.rules, refs, `${ident}-rules` ); } if (rule.oneOf) { newRule.oneOf = RuleSet.normalizeRules( rule.oneOf, refs, `${ident}-oneOf` ); } const keys = Object.keys(rule).filter(key => { return ![ "resource", "resourceQuery", "compiler", "test", "include", "exclude", "issuer", "loader", "options", "query", "loaders", "use", "rules", "oneOf" ].includes(key); }); for (const key of keys) { newRule[key] = rule[key]; } if (Array.isArray(newRule.use)) { for (const item of newRule.use) { if (item.ident) { refs[item.ident] = item.options; } } } return newRule; } static buildErrorMessage(condition, error) { const conditionAsText = JSON.stringify( condition, (key, value) => { return value === undefined ? "undefined" : value; }, 2 ); return error.message + " in " + conditionAsText; } static normalizeUse(use, ident) { if (typeof use === "function") { return data => RuleSet.normalizeUse(use(data), ident); } if (Array.isArray(use)) { return use .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`)) .reduce((arr, items) => arr.concat(items), []); } return [RuleSet.normalizeUseItem(use, ident)]; } static normalizeUseItemString(useItemString) { const idx = useItemString.indexOf("?"); if (idx >= 0) { return { loader: useItemString.substr(0, idx), options: useItemString.substr(idx + 1) }; } return { loader: useItemString, options: undefined }; } static normalizeUseItem(item, ident) { if (typeof item === "string") { return RuleSet.normalizeUseItemString(item); } const newItem = {}; if (item.options && item.query) { throw new Error("Provided options and query in use"); } if (!item.loader) { throw new Error("No loader specified"); } newItem.options = item.options || item.query; if (typeof newItem.options === "object" && newItem.options) { if (newItem.options.ident) { newItem.ident = newItem.options.ident; } else { newItem.ident = ident; } } const keys = Object.keys(item).filter(function(key) { return !["options", "query"].includes(key); }); for (const key of keys) { newItem[key] = item[key]; } return newItem; } static normalizeCondition(condition) { if (!condition) throw new Error("Expected condition but got falsy value"); if (typeof condition === "string") { return str => str.indexOf(condition) === 0; } if (typeof condition === "function") { return condition; } if (condition instanceof RegExp) { return condition.test.bind(condition); } if (Array.isArray(condition)) { const items = condition.map(c => RuleSet.normalizeCondition(c)); return orMatcher(items); } if (typeof condition !== "object") { throw Error( "Unexcepted " + typeof condition + " when condition was expected (" + condition + ")" ); } const matchers = []; Object.keys(condition).forEach(key => { const value = condition[key]; switch (key) { case "or": case "include": case "test": if (value) matchers.push(RuleSet.normalizeCondition(value)); break; case "and": if (value) { const items = value.map(c => RuleSet.normalizeCondition(c)); matchers.push(andMatcher(items)); } break; case "not": case "exclude": if (value) { const matcher = RuleSet.normalizeCondition(value); matchers.push(notMatcher(matcher)); } break; default: throw new Error("Unexcepted property " + key + " in condition"); } }); if (matchers.length === 0) { throw new Error("Excepted condition but got " + condition); } if (matchers.length === 1) { return matchers[0]; } return andMatcher(matchers); } exec(data) { const result = []; this._run( data, { rules: this.rules }, result ); return result; } _run(data, rule, result) { // test conditions if (rule.resource && !data.resource) return false; if (rule.realResource && !data.realResource) return false; if (rule.resourceQuery && !data.resourceQuery) return false; if (rule.compiler && !data.compiler) return false; if (rule.issuer && !data.issuer) return false; if (rule.resource && !rule.resource(data.resource)) return false; if (rule.realResource && !rule.realResource(data.realResource)) return false; if (data.issuer && rule.issuer && !rule.issuer(data.issuer)) return false; if ( data.resourceQuery && rule.resourceQuery && !rule.resourceQuery(data.resourceQuery) ) { return false; } if (data.compiler && rule.compiler && !rule.compiler(data.compiler)) { return false; } // apply const keys = Object.keys(rule).filter(key => { return ![ "resource", "realResource", "resourceQuery", "compiler", "issuer", "rules", "oneOf", "use", "enforce" ].includes(key); }); for (const key of keys) { result.push({ type: key, value: rule[key] }); } if (rule.use) { const process = use => { if (typeof use === "function") { process(use(data)); } else if (Array.isArray(use)) { use.forEach(process); } else { result.push({ type: "use", value: use, enforce: rule.enforce }); } }; process(rule.use); } if (rule.rules) { for (let i = 0; i < rule.rules.length; i++) { this._run(data, rule.rules[i], result); } } if (rule.oneOf) { for (let i = 0; i < rule.oneOf.length; i++) { if (this._run(data, rule.oneOf[i], result)) break; } } return true; } findOptionsByIdent(ident) { const options = this.references[ident]; if (!options) { throw new Error("Can't find options with ident '" + ident + "'"); } return options; } };