@halleyassist/rule-templater 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/rule-templater.browser.js +269 -22
- package/index.d.ts +33 -2
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/GeneralTemplate.js +85 -5
- package/src/HalleyFunctionBlob.js +126 -0
- package/src/RuleTemplate.js +174 -18
- package/src/RuleTemplate.production.js +174 -18
- package/src/TemplateFilters.js +88 -0
- package/src/VariableTemplate.js +74 -6
- package/src/VariableValidate.js +7 -4
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
class HalleyFunctionBlob {
|
|
2
|
+
constructor(jsonData) {
|
|
3
|
+
const blobData = jsonData && typeof jsonData === 'object' ? jsonData : {};
|
|
4
|
+
|
|
5
|
+
this._schema = blobData._schema;
|
|
6
|
+
this.version = blobData.version;
|
|
7
|
+
this.functions = [];
|
|
8
|
+
this.functionMap = new Map();
|
|
9
|
+
|
|
10
|
+
const functions = Array.isArray(blobData.functions) ? blobData.functions : [];
|
|
11
|
+
for (const definition of functions) {
|
|
12
|
+
if (!definition || typeof definition.name !== 'string') {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const normalizedDefinition = {
|
|
17
|
+
name: definition.name,
|
|
18
|
+
arguments: Array.isArray(definition.arguments) ? definition.arguments.slice() : []
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
this.functions.push(normalizedDefinition);
|
|
22
|
+
this.functionMap.set(normalizedDefinition.name, normalizedDefinition);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async fromURL(url) {
|
|
27
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
28
|
+
throw new Error('A function blob URL must be provided');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof globalThis.fetch !== 'function') {
|
|
32
|
+
throw new Error('Fetch API is not available');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const response = await globalThis.fetch(url);
|
|
36
|
+
if (!response || !response.ok) {
|
|
37
|
+
throw new Error(`Failed to fetch function blob from '${url}'`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new HalleyFunctionBlob(await response.json());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
validate(functionName, variables = []) {
|
|
44
|
+
const warnings = [];
|
|
45
|
+
const functionDefinition = this.functionMap.get(functionName);
|
|
46
|
+
|
|
47
|
+
if (!functionDefinition) {
|
|
48
|
+
return [`function '${functionName}' does not exist`];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const providedVariables = Array.isArray(variables) ? variables : [];
|
|
52
|
+
const providedCount = providedVariables.length;
|
|
53
|
+
const parameterRange = this._getParameterRange(functionDefinition.arguments);
|
|
54
|
+
|
|
55
|
+
for (let idx = 0; idx < functionDefinition.arguments.length; idx++) {
|
|
56
|
+
const argumentName = functionDefinition.arguments[idx];
|
|
57
|
+
if (argumentName === '...') {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (providedCount > idx || argumentName.endsWith('?')) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
warnings.push(
|
|
66
|
+
`parameter ${idx + 1} of ${functionName} '${argumentName}' is missing, function expects ${parameterRange}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (this._hasTooManyArguments(functionDefinition.arguments, providedCount)) {
|
|
71
|
+
warnings.push(
|
|
72
|
+
`${functionName} received ${providedCount} parameters, function expects ${parameterRange}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return warnings;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_hasTooManyArguments(argumentList, providedCount) {
|
|
80
|
+
const { max } = this._getArgumentBounds(argumentList);
|
|
81
|
+
return max !== Infinity && providedCount > max;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_getParameterRange(argumentList) {
|
|
85
|
+
const { min, max } = this._getArgumentBounds(argumentList);
|
|
86
|
+
|
|
87
|
+
if (max === Infinity) {
|
|
88
|
+
if (min === 0) {
|
|
89
|
+
return 'any number of parameters';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return `at least ${min} ${min === 1 ? 'parameter' : 'parameters'}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (min === max) {
|
|
96
|
+
return `${min} ${min === 1 ? 'parameter' : 'parameters'}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `${min} to ${max} parameters`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_getArgumentBounds(argumentList) {
|
|
103
|
+
let min = 0;
|
|
104
|
+
let max = 0;
|
|
105
|
+
let variadic = false;
|
|
106
|
+
|
|
107
|
+
for (const argumentName of argumentList) {
|
|
108
|
+
if (argumentName === '...') {
|
|
109
|
+
variadic = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
max++;
|
|
114
|
+
if (!argumentName.endsWith('?')) {
|
|
115
|
+
min++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
min,
|
|
121
|
+
max: variadic ? Infinity : max
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = HalleyFunctionBlob;
|
package/src/RuleTemplate.js
CHANGED
|
@@ -185,11 +185,13 @@ class RuleTemplate {
|
|
|
185
185
|
|
|
186
186
|
// Extract filters
|
|
187
187
|
const filters = [];
|
|
188
|
+
const filterCalls = [];
|
|
188
189
|
for (const child of templateExpr.children || []) {
|
|
189
190
|
if (child.type === 'template_filter_call') {
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
filters.push(
|
|
191
|
+
const filterCall = this._extractFilterCall(child);
|
|
192
|
+
if (filterCall) {
|
|
193
|
+
filters.push(filterCall.name);
|
|
194
|
+
filterCalls.push(filterCall);
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
}
|
|
@@ -198,38 +200,128 @@ class RuleTemplate {
|
|
|
198
200
|
const start = node.start;
|
|
199
201
|
const end = node.end;
|
|
200
202
|
|
|
201
|
-
return { name, filters, start, end };
|
|
203
|
+
return { name, filters, filterCalls, start, end };
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
/**
|
|
205
|
-
* Extract filter
|
|
207
|
+
* Extract filter call from template_filter_call node
|
|
206
208
|
* @private
|
|
207
209
|
*/
|
|
208
|
-
|
|
210
|
+
_extractFilterCall(node) {
|
|
209
211
|
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
210
212
|
if (!filterNameNode || !filterNameNode.text) return null;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
|
|
214
|
+
const argsNode = node.children?.find(c => c.type === 'template_filter_args');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name: filterNameNode.text.trim(),
|
|
218
|
+
args: this._extractFilterArgs(argsNode)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_extractFilterArgs(node) {
|
|
223
|
+
if (!node || !Array.isArray(node.children)) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return node.children
|
|
228
|
+
.filter(child => child.type === 'template_filter_arg')
|
|
229
|
+
.map(child => this._extractFilterArgValue(child));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_extractFilterArgValue(node) {
|
|
233
|
+
if (!node || !Array.isArray(node.children) || node.children.length === 0) {
|
|
234
|
+
return this._normalizeFilterArgText(node?.text?.trim() || '');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const child = node.children[0];
|
|
238
|
+
if (!child) {
|
|
239
|
+
return this._normalizeFilterArgText(node.text?.trim() || '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
|
|
243
|
+
return this._extractFilterArgValue(child);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (child.type === 'string') {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(child.text);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return this._normalizeFilterArgText(child.text);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (child.type === 'number') {
|
|
255
|
+
return Number(child.text);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (child.type === 'true') {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (child.type === 'false') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (child.type === 'null') {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_normalizeFilterArgText(text) {
|
|
274
|
+
const normalizedText = String(text).trim();
|
|
275
|
+
|
|
276
|
+
if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
|
|
277
|
+
return normalizedText.slice(1, -1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return normalizedText;
|
|
213
281
|
}
|
|
214
282
|
|
|
215
283
|
/**
|
|
216
284
|
* Validate variable types against the AST
|
|
217
285
|
* @param {Object} variables - Object mapping variable names to {type} objects
|
|
218
|
-
* @
|
|
286
|
+
* @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
|
|
287
|
+
* @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
|
|
219
288
|
*/
|
|
220
|
-
validate(variables) {
|
|
289
|
+
validate(variables, functionBlob) {
|
|
221
290
|
if (!variables || typeof variables !== 'object') {
|
|
222
291
|
return {
|
|
223
292
|
valid: false,
|
|
224
|
-
errors: ['Variables must be provided as an object']
|
|
293
|
+
errors: ['Variables must be provided as an object'],
|
|
294
|
+
warnings: []
|
|
225
295
|
};
|
|
226
296
|
}
|
|
227
297
|
|
|
228
298
|
const errors = [];
|
|
229
|
-
const
|
|
299
|
+
const warnings = [];
|
|
300
|
+
const extractedVars = this._extractTemplateVariables();
|
|
301
|
+
const seenVariables = new Set();
|
|
302
|
+
const seenFilterErrors = new Set();
|
|
230
303
|
|
|
231
304
|
for (const varInfo of extractedVars) {
|
|
232
305
|
const varName = varInfo.name;
|
|
306
|
+
|
|
307
|
+
for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
|
|
308
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
309
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
|
|
314
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
315
|
+
errors.push(errorMessage);
|
|
316
|
+
seenFilterErrors.add(errorMessage);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (seenVariables.has(varName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
seenVariables.add(varName);
|
|
233
325
|
|
|
234
326
|
// Check if variable is provided
|
|
235
327
|
if (!variables.hasOwnProperty(varName)) {
|
|
@@ -258,11 +350,72 @@ class RuleTemplate {
|
|
|
258
350
|
}
|
|
259
351
|
}
|
|
260
352
|
}
|
|
353
|
+
|
|
354
|
+
if (functionBlob && typeof functionBlob.validate === 'function') {
|
|
355
|
+
for (const functionCall of this._extractFunctionCalls()) {
|
|
356
|
+
warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
261
359
|
|
|
262
360
|
return {
|
|
263
361
|
valid: errors.length === 0,
|
|
264
|
-
errors
|
|
362
|
+
errors,
|
|
363
|
+
warnings
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_extractFunctionCalls() {
|
|
368
|
+
const functionCalls = [];
|
|
369
|
+
|
|
370
|
+
const traverse = (node) => {
|
|
371
|
+
if (!node) return;
|
|
372
|
+
|
|
373
|
+
if (node.type === 'fcall') {
|
|
374
|
+
const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
|
|
375
|
+
const argumentsNode = node.children?.find(c => c.type === 'arguments');
|
|
376
|
+
if (functionName) {
|
|
377
|
+
functionCalls.push({
|
|
378
|
+
name: functionName,
|
|
379
|
+
arguments: argumentsNode?.children
|
|
380
|
+
?.filter(c => c.type === 'argument')
|
|
381
|
+
.map(c => c.text) || []
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (node.children) {
|
|
387
|
+
for (const child of node.children) {
|
|
388
|
+
traverse(child);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
265
391
|
};
|
|
392
|
+
|
|
393
|
+
traverse(this.ast);
|
|
394
|
+
return functionCalls;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_extractTemplateVariables() {
|
|
398
|
+
const variables = [];
|
|
399
|
+
|
|
400
|
+
const traverse = (node) => {
|
|
401
|
+
if (!node) return;
|
|
402
|
+
|
|
403
|
+
if (node.type === 'template_value') {
|
|
404
|
+
const variableInfo = this._extractVariableFromNode(node);
|
|
405
|
+
if (variableInfo) {
|
|
406
|
+
variables.push(variableInfo);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (node.children) {
|
|
411
|
+
for (const child of node.children) {
|
|
412
|
+
traverse(child);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
traverse(this.ast);
|
|
418
|
+
return variables;
|
|
266
419
|
}
|
|
267
420
|
|
|
268
421
|
/**
|
|
@@ -367,12 +520,15 @@ class RuleTemplate {
|
|
|
367
520
|
|
|
368
521
|
// Apply filters if present
|
|
369
522
|
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
370
|
-
for (const
|
|
371
|
-
|
|
372
|
-
|
|
523
|
+
for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
|
|
524
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
525
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
526
|
+
|
|
527
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
528
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
373
529
|
}
|
|
374
|
-
|
|
375
|
-
TemplateFilters[filterName](varData);
|
|
530
|
+
|
|
531
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
376
532
|
}
|
|
377
533
|
}
|
|
378
534
|
|
|
@@ -185,11 +185,13 @@ class RuleTemplate {
|
|
|
185
185
|
|
|
186
186
|
// Extract filters
|
|
187
187
|
const filters = [];
|
|
188
|
+
const filterCalls = [];
|
|
188
189
|
for (const child of templateExpr.children || []) {
|
|
189
190
|
if (child.type === 'template_filter_call') {
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
filters.push(
|
|
191
|
+
const filterCall = this._extractFilterCall(child);
|
|
192
|
+
if (filterCall) {
|
|
193
|
+
filters.push(filterCall.name);
|
|
194
|
+
filterCalls.push(filterCall);
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
}
|
|
@@ -198,38 +200,128 @@ class RuleTemplate {
|
|
|
198
200
|
const start = node.start;
|
|
199
201
|
const end = node.end;
|
|
200
202
|
|
|
201
|
-
return { name, filters, start, end };
|
|
203
|
+
return { name, filters, filterCalls, start, end };
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
/**
|
|
205
|
-
* Extract filter
|
|
207
|
+
* Extract filter call from template_filter_call node
|
|
206
208
|
* @private
|
|
207
209
|
*/
|
|
208
|
-
|
|
210
|
+
_extractFilterCall(node) {
|
|
209
211
|
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
210
212
|
if (!filterNameNode || !filterNameNode.text) return null;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
|
|
214
|
+
const argsNode = node.children?.find(c => c.type === 'template_filter_args');
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
name: filterNameNode.text.trim(),
|
|
218
|
+
args: this._extractFilterArgs(argsNode)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
_extractFilterArgs(node) {
|
|
223
|
+
if (!node || !Array.isArray(node.children)) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return node.children
|
|
228
|
+
.filter(child => child.type === 'template_filter_arg')
|
|
229
|
+
.map(child => this._extractFilterArgValue(child));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_extractFilterArgValue(node) {
|
|
233
|
+
if (!node || !Array.isArray(node.children) || node.children.length === 0) {
|
|
234
|
+
return this._normalizeFilterArgText(node?.text?.trim() || '');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const child = node.children[0];
|
|
238
|
+
if (!child) {
|
|
239
|
+
return this._normalizeFilterArgText(node.text?.trim() || '');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
|
|
243
|
+
return this._extractFilterArgValue(child);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (child.type === 'string') {
|
|
247
|
+
try {
|
|
248
|
+
return JSON.parse(child.text);
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return this._normalizeFilterArgText(child.text);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (child.type === 'number') {
|
|
255
|
+
return Number(child.text);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (child.type === 'true') {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (child.type === 'false') {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (child.type === 'null') {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_normalizeFilterArgText(text) {
|
|
274
|
+
const normalizedText = String(text).trim();
|
|
275
|
+
|
|
276
|
+
if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
|
|
277
|
+
return normalizedText.slice(1, -1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return normalizedText;
|
|
213
281
|
}
|
|
214
282
|
|
|
215
283
|
/**
|
|
216
284
|
* Validate variable types against the AST
|
|
217
285
|
* @param {Object} variables - Object mapping variable names to {type} objects
|
|
218
|
-
* @
|
|
286
|
+
* @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
|
|
287
|
+
* @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
|
|
219
288
|
*/
|
|
220
|
-
validate(variables) {
|
|
289
|
+
validate(variables, functionBlob) {
|
|
221
290
|
if (!variables || typeof variables !== 'object') {
|
|
222
291
|
return {
|
|
223
292
|
valid: false,
|
|
224
|
-
errors: ['Variables must be provided as an object']
|
|
293
|
+
errors: ['Variables must be provided as an object'],
|
|
294
|
+
warnings: []
|
|
225
295
|
};
|
|
226
296
|
}
|
|
227
297
|
|
|
228
298
|
const errors = [];
|
|
229
|
-
const
|
|
299
|
+
const warnings = [];
|
|
300
|
+
const extractedVars = this._extractTemplateVariables();
|
|
301
|
+
const seenVariables = new Set();
|
|
302
|
+
const seenFilterErrors = new Set();
|
|
230
303
|
|
|
231
304
|
for (const varInfo of extractedVars) {
|
|
232
305
|
const varName = varInfo.name;
|
|
306
|
+
|
|
307
|
+
for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
|
|
308
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
309
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
|
|
314
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
315
|
+
errors.push(errorMessage);
|
|
316
|
+
seenFilterErrors.add(errorMessage);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (seenVariables.has(varName)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
seenVariables.add(varName);
|
|
233
325
|
|
|
234
326
|
// Check if variable is provided
|
|
235
327
|
if (!variables.hasOwnProperty(varName)) {
|
|
@@ -258,11 +350,72 @@ class RuleTemplate {
|
|
|
258
350
|
}
|
|
259
351
|
}
|
|
260
352
|
}
|
|
353
|
+
|
|
354
|
+
if (functionBlob && typeof functionBlob.validate === 'function') {
|
|
355
|
+
for (const functionCall of this._extractFunctionCalls()) {
|
|
356
|
+
warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
261
359
|
|
|
262
360
|
return {
|
|
263
361
|
valid: errors.length === 0,
|
|
264
|
-
errors
|
|
362
|
+
errors,
|
|
363
|
+
warnings
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_extractFunctionCalls() {
|
|
368
|
+
const functionCalls = [];
|
|
369
|
+
|
|
370
|
+
const traverse = (node) => {
|
|
371
|
+
if (!node) return;
|
|
372
|
+
|
|
373
|
+
if (node.type === 'fcall') {
|
|
374
|
+
const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
|
|
375
|
+
const argumentsNode = node.children?.find(c => c.type === 'arguments');
|
|
376
|
+
if (functionName) {
|
|
377
|
+
functionCalls.push({
|
|
378
|
+
name: functionName,
|
|
379
|
+
arguments: argumentsNode?.children
|
|
380
|
+
?.filter(c => c.type === 'argument')
|
|
381
|
+
.map(c => c.text) || []
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (node.children) {
|
|
387
|
+
for (const child of node.children) {
|
|
388
|
+
traverse(child);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
265
391
|
};
|
|
392
|
+
|
|
393
|
+
traverse(this.ast);
|
|
394
|
+
return functionCalls;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
_extractTemplateVariables() {
|
|
398
|
+
const variables = [];
|
|
399
|
+
|
|
400
|
+
const traverse = (node) => {
|
|
401
|
+
if (!node) return;
|
|
402
|
+
|
|
403
|
+
if (node.type === 'template_value') {
|
|
404
|
+
const variableInfo = this._extractVariableFromNode(node);
|
|
405
|
+
if (variableInfo) {
|
|
406
|
+
variables.push(variableInfo);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (node.children) {
|
|
411
|
+
for (const child of node.children) {
|
|
412
|
+
traverse(child);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
traverse(this.ast);
|
|
418
|
+
return variables;
|
|
266
419
|
}
|
|
267
420
|
|
|
268
421
|
/**
|
|
@@ -367,12 +520,15 @@ class RuleTemplate {
|
|
|
367
520
|
|
|
368
521
|
// Apply filters if present
|
|
369
522
|
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
370
|
-
for (const
|
|
371
|
-
|
|
372
|
-
|
|
523
|
+
for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
|
|
524
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
525
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
526
|
+
|
|
527
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
528
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
373
529
|
}
|
|
374
|
-
|
|
375
|
-
TemplateFilters[filterName](varData);
|
|
530
|
+
|
|
531
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
376
532
|
}
|
|
377
533
|
}
|
|
378
534
|
|