1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | "use strict";
|
76 |
|
77 | const notMatcher = matcher => {
|
78 | return str => {
|
79 | return !matcher(str);
|
80 | };
|
81 | };
|
82 |
|
83 | const orMatcher = items => {
|
84 | return str => {
|
85 | for (let i = 0; i < items.length; i++) {
|
86 | if (items[i](str)) return true;
|
87 | }
|
88 | return false;
|
89 | };
|
90 | };
|
91 |
|
92 | const andMatcher = items => {
|
93 | return str => {
|
94 | for (let i = 0; i < items.length; i++) {
|
95 | if (!items[i](str)) return false;
|
96 | }
|
97 | return true;
|
98 | };
|
99 | };
|
100 |
|
101 | module.exports = class RuleSet {
|
102 | constructor(rules) {
|
103 | this.references = Object.create(null);
|
104 | this.rules = RuleSet.normalizeRules(rules, this.references, "ref-");
|
105 | }
|
106 |
|
107 | static normalizeRules(rules, refs, ident) {
|
108 | if (Array.isArray(rules)) {
|
109 | return rules.map((rule, idx) => {
|
110 | return RuleSet.normalizeRule(rule, refs, `${ident}-${idx}`);
|
111 | });
|
112 | } else if (rules) {
|
113 | return [RuleSet.normalizeRule(rules, refs, ident)];
|
114 | } else {
|
115 | return [];
|
116 | }
|
117 | }
|
118 |
|
119 | static normalizeRule(rule, refs, ident) {
|
120 | if (typeof rule === "string") {
|
121 | return {
|
122 | use: [
|
123 | {
|
124 | loader: rule
|
125 | }
|
126 | ]
|
127 | };
|
128 | }
|
129 | if (!rule) {
|
130 | throw new Error("Unexcepted null when object was expected as rule");
|
131 | }
|
132 | if (typeof rule !== "object") {
|
133 | throw new Error(
|
134 | "Unexcepted " +
|
135 | typeof rule +
|
136 | " when object was expected as rule (" +
|
137 | rule +
|
138 | ")"
|
139 | );
|
140 | }
|
141 |
|
142 | const newRule = {};
|
143 | let useSource;
|
144 | let resourceSource;
|
145 | let condition;
|
146 |
|
147 | const checkUseSource = newSource => {
|
148 | if (useSource && useSource !== newSource) {
|
149 | throw new Error(
|
150 | RuleSet.buildErrorMessage(
|
151 | rule,
|
152 | new Error(
|
153 | "Rule can only have one result source (provided " +
|
154 | newSource +
|
155 | " and " +
|
156 | useSource +
|
157 | ")"
|
158 | )
|
159 | )
|
160 | );
|
161 | }
|
162 | useSource = newSource;
|
163 | };
|
164 |
|
165 | const checkResourceSource = newSource => {
|
166 | if (resourceSource && resourceSource !== newSource) {
|
167 | throw new Error(
|
168 | RuleSet.buildErrorMessage(
|
169 | rule,
|
170 | new Error(
|
171 | "Rule can only have one resource source (provided " +
|
172 | newSource +
|
173 | " and " +
|
174 | resourceSource +
|
175 | ")"
|
176 | )
|
177 | )
|
178 | );
|
179 | }
|
180 | resourceSource = newSource;
|
181 | };
|
182 |
|
183 | if (rule.test || rule.include || rule.exclude) {
|
184 | checkResourceSource("test + include + exclude");
|
185 | condition = {
|
186 | test: rule.test,
|
187 | include: rule.include,
|
188 | exclude: rule.exclude
|
189 | };
|
190 | try {
|
191 | newRule.resource = RuleSet.normalizeCondition(condition);
|
192 | } catch (error) {
|
193 | throw new Error(RuleSet.buildErrorMessage(condition, error));
|
194 | }
|
195 | }
|
196 |
|
197 | if (rule.resource) {
|
198 | checkResourceSource("resource");
|
199 | try {
|
200 | newRule.resource = RuleSet.normalizeCondition(rule.resource);
|
201 | } catch (error) {
|
202 | throw new Error(RuleSet.buildErrorMessage(rule.resource, error));
|
203 | }
|
204 | }
|
205 |
|
206 | if (rule.realResource) {
|
207 | try {
|
208 | newRule.realResource = RuleSet.normalizeCondition(rule.realResource);
|
209 | } catch (error) {
|
210 | throw new Error(RuleSet.buildErrorMessage(rule.realResource, error));
|
211 | }
|
212 | }
|
213 |
|
214 | if (rule.resourceQuery) {
|
215 | try {
|
216 | newRule.resourceQuery = RuleSet.normalizeCondition(rule.resourceQuery);
|
217 | } catch (error) {
|
218 | throw new Error(RuleSet.buildErrorMessage(rule.resourceQuery, error));
|
219 | }
|
220 | }
|
221 |
|
222 | if (rule.compiler) {
|
223 | try {
|
224 | newRule.compiler = RuleSet.normalizeCondition(rule.compiler);
|
225 | } catch (error) {
|
226 | throw new Error(RuleSet.buildErrorMessage(rule.compiler, error));
|
227 | }
|
228 | }
|
229 |
|
230 | if (rule.issuer) {
|
231 | try {
|
232 | newRule.issuer = RuleSet.normalizeCondition(rule.issuer);
|
233 | } catch (error) {
|
234 | throw new Error(RuleSet.buildErrorMessage(rule.issuer, error));
|
235 | }
|
236 | }
|
237 |
|
238 | if (rule.loader && rule.loaders) {
|
239 | throw new Error(
|
240 | RuleSet.buildErrorMessage(
|
241 | rule,
|
242 | new Error(
|
243 | "Provided loader and loaders for rule (use only one of them)"
|
244 | )
|
245 | )
|
246 | );
|
247 | }
|
248 |
|
249 | const loader = rule.loaders || rule.loader;
|
250 | if (typeof loader === "string" && !rule.options && !rule.query) {
|
251 | checkUseSource("loader");
|
252 | newRule.use = RuleSet.normalizeUse(loader.split("!"), ident);
|
253 | } else if (typeof loader === "string" && (rule.options || rule.query)) {
|
254 | checkUseSource("loader + options/query");
|
255 | newRule.use = RuleSet.normalizeUse(
|
256 | {
|
257 | loader: loader,
|
258 | options: rule.options,
|
259 | query: rule.query
|
260 | },
|
261 | ident
|
262 | );
|
263 | } else if (loader && (rule.options || rule.query)) {
|
264 | throw new Error(
|
265 | RuleSet.buildErrorMessage(
|
266 | rule,
|
267 | new Error(
|
268 | "options/query cannot be used with loaders (use options for each array item)"
|
269 | )
|
270 | )
|
271 | );
|
272 | } else if (loader) {
|
273 | checkUseSource("loaders");
|
274 | newRule.use = RuleSet.normalizeUse(loader, ident);
|
275 | } else if (rule.options || rule.query) {
|
276 | throw new Error(
|
277 | RuleSet.buildErrorMessage(
|
278 | rule,
|
279 | new Error(
|
280 | "options/query provided without loader (use loader + options)"
|
281 | )
|
282 | )
|
283 | );
|
284 | }
|
285 |
|
286 | if (rule.use) {
|
287 | checkUseSource("use");
|
288 | newRule.use = RuleSet.normalizeUse(rule.use, ident);
|
289 | }
|
290 |
|
291 | if (rule.rules) {
|
292 | newRule.rules = RuleSet.normalizeRules(
|
293 | rule.rules,
|
294 | refs,
|
295 | `${ident}-rules`
|
296 | );
|
297 | }
|
298 |
|
299 | if (rule.oneOf) {
|
300 | newRule.oneOf = RuleSet.normalizeRules(
|
301 | rule.oneOf,
|
302 | refs,
|
303 | `${ident}-oneOf`
|
304 | );
|
305 | }
|
306 |
|
307 | const keys = Object.keys(rule).filter(key => {
|
308 | return ![
|
309 | "resource",
|
310 | "resourceQuery",
|
311 | "compiler",
|
312 | "test",
|
313 | "include",
|
314 | "exclude",
|
315 | "issuer",
|
316 | "loader",
|
317 | "options",
|
318 | "query",
|
319 | "loaders",
|
320 | "use",
|
321 | "rules",
|
322 | "oneOf"
|
323 | ].includes(key);
|
324 | });
|
325 | for (const key of keys) {
|
326 | newRule[key] = rule[key];
|
327 | }
|
328 |
|
329 | if (Array.isArray(newRule.use)) {
|
330 | for (const item of newRule.use) {
|
331 | if (item.ident) {
|
332 | refs[item.ident] = item.options;
|
333 | }
|
334 | }
|
335 | }
|
336 |
|
337 | return newRule;
|
338 | }
|
339 |
|
340 | static buildErrorMessage(condition, error) {
|
341 | const conditionAsText = JSON.stringify(
|
342 | condition,
|
343 | (key, value) => {
|
344 | return value === undefined ? "undefined" : value;
|
345 | },
|
346 | 2
|
347 | );
|
348 | return error.message + " in " + conditionAsText;
|
349 | }
|
350 |
|
351 | static normalizeUse(use, ident) {
|
352 | if (typeof use === "function") {
|
353 | return data => RuleSet.normalizeUse(use(data), ident);
|
354 | }
|
355 | if (Array.isArray(use)) {
|
356 | return use
|
357 | .map((item, idx) => RuleSet.normalizeUse(item, `${ident}-${idx}`))
|
358 | .reduce((arr, items) => arr.concat(items), []);
|
359 | }
|
360 | return [RuleSet.normalizeUseItem(use, ident)];
|
361 | }
|
362 |
|
363 | static normalizeUseItemString(useItemString) {
|
364 | const idx = useItemString.indexOf("?");
|
365 | if (idx >= 0) {
|
366 | return {
|
367 | loader: useItemString.substr(0, idx),
|
368 | options: useItemString.substr(idx + 1)
|
369 | };
|
370 | }
|
371 | return {
|
372 | loader: useItemString,
|
373 | options: undefined
|
374 | };
|
375 | }
|
376 |
|
377 | static normalizeUseItem(item, ident) {
|
378 | if (typeof item === "string") {
|
379 | return RuleSet.normalizeUseItemString(item);
|
380 | }
|
381 |
|
382 | const newItem = {};
|
383 |
|
384 | if (item.options && item.query) {
|
385 | throw new Error("Provided options and query in use");
|
386 | }
|
387 |
|
388 | if (!item.loader) {
|
389 | throw new Error("No loader specified");
|
390 | }
|
391 |
|
392 | newItem.options = item.options || item.query;
|
393 |
|
394 | if (typeof newItem.options === "object" && newItem.options) {
|
395 | if (newItem.options.ident) {
|
396 | newItem.ident = newItem.options.ident;
|
397 | } else {
|
398 | newItem.ident = ident;
|
399 | }
|
400 | }
|
401 |
|
402 | const keys = Object.keys(item).filter(function(key) {
|
403 | return !["options", "query"].includes(key);
|
404 | });
|
405 |
|
406 | for (const key of keys) {
|
407 | newItem[key] = item[key];
|
408 | }
|
409 |
|
410 | return newItem;
|
411 | }
|
412 |
|
413 | static normalizeCondition(condition) {
|
414 | if (!condition) throw new Error("Expected condition but got falsy value");
|
415 | if (typeof condition === "string") {
|
416 | return str => str.indexOf(condition) === 0;
|
417 | }
|
418 | if (typeof condition === "function") {
|
419 | return condition;
|
420 | }
|
421 | if (condition instanceof RegExp) {
|
422 | return condition.test.bind(condition);
|
423 | }
|
424 | if (Array.isArray(condition)) {
|
425 | const items = condition.map(c => RuleSet.normalizeCondition(c));
|
426 | return orMatcher(items);
|
427 | }
|
428 | if (typeof condition !== "object") {
|
429 | throw Error(
|
430 | "Unexcepted " +
|
431 | typeof condition +
|
432 | " when condition was expected (" +
|
433 | condition +
|
434 | ")"
|
435 | );
|
436 | }
|
437 |
|
438 | const matchers = [];
|
439 | Object.keys(condition).forEach(key => {
|
440 | const value = condition[key];
|
441 | switch (key) {
|
442 | case "or":
|
443 | case "include":
|
444 | case "test":
|
445 | if (value) matchers.push(RuleSet.normalizeCondition(value));
|
446 | break;
|
447 | case "and":
|
448 | if (value) {
|
449 | const items = value.map(c => RuleSet.normalizeCondition(c));
|
450 | matchers.push(andMatcher(items));
|
451 | }
|
452 | break;
|
453 | case "not":
|
454 | case "exclude":
|
455 | if (value) {
|
456 | const matcher = RuleSet.normalizeCondition(value);
|
457 | matchers.push(notMatcher(matcher));
|
458 | }
|
459 | break;
|
460 | default:
|
461 | throw new Error("Unexcepted property " + key + " in condition");
|
462 | }
|
463 | });
|
464 | if (matchers.length === 0) {
|
465 | throw new Error("Excepted condition but got " + condition);
|
466 | }
|
467 | if (matchers.length === 1) {
|
468 | return matchers[0];
|
469 | }
|
470 | return andMatcher(matchers);
|
471 | }
|
472 |
|
473 | exec(data) {
|
474 | const result = [];
|
475 | this._run(
|
476 | data,
|
477 | {
|
478 | rules: this.rules
|
479 | },
|
480 | result
|
481 | );
|
482 | return result;
|
483 | }
|
484 |
|
485 | _run(data, rule, result) {
|
486 |
|
487 | if (rule.resource && !data.resource) return false;
|
488 | if (rule.realResource && !data.realResource) return false;
|
489 | if (rule.resourceQuery && !data.resourceQuery) return false;
|
490 | if (rule.compiler && !data.compiler) return false;
|
491 | if (rule.issuer && !data.issuer) return false;
|
492 | if (rule.resource && !rule.resource(data.resource)) return false;
|
493 | if (rule.realResource && !rule.realResource(data.realResource))
|
494 | return false;
|
495 | if (data.issuer && rule.issuer && !rule.issuer(data.issuer)) return false;
|
496 | if (
|
497 | data.resourceQuery &&
|
498 | rule.resourceQuery &&
|
499 | !rule.resourceQuery(data.resourceQuery)
|
500 | ) {
|
501 | return false;
|
502 | }
|
503 | if (data.compiler && rule.compiler && !rule.compiler(data.compiler)) {
|
504 | return false;
|
505 | }
|
506 |
|
507 |
|
508 | const keys = Object.keys(rule).filter(key => {
|
509 | return ![
|
510 | "resource",
|
511 | "realResource",
|
512 | "resourceQuery",
|
513 | "compiler",
|
514 | "issuer",
|
515 | "rules",
|
516 | "oneOf",
|
517 | "use",
|
518 | "enforce"
|
519 | ].includes(key);
|
520 | });
|
521 | for (const key of keys) {
|
522 | result.push({
|
523 | type: key,
|
524 | value: rule[key]
|
525 | });
|
526 | }
|
527 |
|
528 | if (rule.use) {
|
529 | const process = use => {
|
530 | if (typeof use === "function") {
|
531 | process(use(data));
|
532 | } else if (Array.isArray(use)) {
|
533 | use.forEach(process);
|
534 | } else {
|
535 | result.push({
|
536 | type: "use",
|
537 | value: use,
|
538 | enforce: rule.enforce
|
539 | });
|
540 | }
|
541 | };
|
542 | process(rule.use);
|
543 | }
|
544 |
|
545 | if (rule.rules) {
|
546 | for (let i = 0; i < rule.rules.length; i++) {
|
547 | this._run(data, rule.rules[i], result);
|
548 | }
|
549 | }
|
550 |
|
551 | if (rule.oneOf) {
|
552 | for (let i = 0; i < rule.oneOf.length; i++) {
|
553 | if (this._run(data, rule.oneOf[i], result)) break;
|
554 | }
|
555 | }
|
556 |
|
557 | return true;
|
558 | }
|
559 |
|
560 | findOptionsByIdent(ident) {
|
561 | const options = this.references[ident];
|
562 | if (!options) {
|
563 | throw new Error("Can't find options with ident '" + ident + "'");
|
564 | }
|
565 | return options;
|
566 | }
|
567 | };
|