Projekt

Obecné

Profil

Stáhnout (11.8 KB) Statistiky
| Větev: | Revize:
1
/*
2
	MIT License http://www.opensource.org/licenses/mit-license.php
3
	Author Tobias Koppers @sokra
4
*/
5
/*
6
<rules>: <rule>
7
<rules>: [<rule>]
8
<rule>: {
9
	resource: {
10
		test: <condition>,
11
		include: <condition>,
12
		exclude: <condition>,
13
	},
14
	resource: <condition>, -> resource.test
15
	test: <condition>, -> resource.test
16
	include: <condition>, -> resource.include
17
	exclude: <condition>, -> resource.exclude
18
	resourceQuery: <condition>,
19
	compiler: <condition>,
20
	issuer: <condition>,
21
	use: "loader", -> use[0].loader
22
	loader: <>, -> use[0].loader
23
	loaders: <>, -> use
24
	options: {}, -> use[0].options,
25
	query: {}, -> options
26
	parser: {},
27
	use: [
28
		"loader" -> use[x].loader
29
	],
30
	use: [
31
		{
32
			loader: "loader",
33
			options: {}
34
		}
35
	],
36
	rules: [
37
		<rule>
38
	],
39
	oneOf: [
40
		<rule>
41
	]
42
}
43

    
44
<condition>: /regExp/
45
<condition>: function(arg) {}
46
<condition>: "starting"
47
<condition>: [<condition>] // or
48
<condition>: { and: [<condition>] }
49
<condition>: { or: [<condition>] }
50
<condition>: { not: [<condition>] }
51
<condition>: { test: <condition>, include: <condition>, exclude: <condition> }
52

    
53

    
54
normalized:
55

    
56
{
57
	resource: function(),
58
	resourceQuery: function(),
59
	compiler: function(),
60
	issuer: function(),
61
	use: [
62
		{
63
			loader: string,
64
			options: string,
65
			<any>: <any>
66
		}
67
	],
68
	rules: [<rule>],
69
	oneOf: [<rule>],
70
	<any>: <any>,
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
		// test conditions
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
		// apply
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
};
(117-117/144)