@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
package/README.md
CHANGED
|
@@ -121,6 +121,8 @@ const prepared = parsed.prepare({
|
|
|
121
121
|
- **round**: Round number to nearest integer
|
|
122
122
|
- **floor**: Round number down
|
|
123
123
|
- **ceil**: Round number up
|
|
124
|
+
- **humanise_list**: Join array values into natural language, optionally with a custom joiner like `humanise_list("or")`
|
|
125
|
+
- **humanise_time**: Convert seconds into a single human-readable unit like `1 hour`; optionally round down to a minimum unit such as `humanise_time("minute")` or `humanise_time("min")`
|
|
124
126
|
- **time_start**: Extract `from` from `time period` / `time period ago` and convert to `time value`
|
|
125
127
|
- **time_end**: Extract `to` from `time period` / `time period ago` and convert to `time value`
|
|
126
128
|
|
|
@@ -140,6 +142,14 @@ const prepared = parsed.prepare({
|
|
|
140
142
|
// Chaining filters
|
|
141
143
|
'${text|trim|upper}' with text=' hello ' → HELLO
|
|
142
144
|
|
|
145
|
+
// Humanise arrays
|
|
146
|
+
'${names|humanise_list}' with names=['a','b','c'] → a, b and c
|
|
147
|
+
'${names|humanise_list("or")}' with names=['a','b','c'] → a, b or c
|
|
148
|
+
|
|
149
|
+
// Humanise time from seconds
|
|
150
|
+
'${duration|humanise_time}' with duration=3600 → 1 hour
|
|
151
|
+
'${duration|humanise_time("minute")}' with duration=71 → 1 minute
|
|
152
|
+
|
|
143
153
|
// Time period conversion
|
|
144
154
|
'${window|time_start}' with window={from:'08:00',to:'12:00'} → 08:00
|
|
145
155
|
```
|
|
@@ -232,7 +242,7 @@ This is useful for comparing against a hub's list of available functions to ensu
|
|
|
232
242
|
|
|
233
243
|
### `ruleTemplate.validate(variables)`
|
|
234
244
|
|
|
235
|
-
Validates that all required variables are provided
|
|
245
|
+
Validates that all required variables are provided, have valid types, and reference only known template filters.
|
|
236
246
|
|
|
237
247
|
**Parameters:**
|
|
238
248
|
- `variables` (object): Object mapping variable names to their values and types
|
|
@@ -243,6 +253,7 @@ Validates that all required variables are provided and have valid types.
|
|
|
243
253
|
**Returns:** Object with:
|
|
244
254
|
- `valid` (boolean): Whether validation passed
|
|
245
255
|
- `errors` (array): Array of error messages (empty if valid)
|
|
256
|
+
- `warnings` (array): Array of non-fatal warnings (empty if none)
|
|
246
257
|
|
|
247
258
|
### `ruleTemplate.prepare(variables)`
|
|
248
259
|
|
|
@@ -272,6 +283,10 @@ Extracts variables from a general string template.
|
|
|
272
283
|
|
|
273
284
|
Prepares a general string template by replacing `${...}` placeholders with values and applying filters.
|
|
274
285
|
|
|
286
|
+
### `generalTemplate.validate()`
|
|
287
|
+
|
|
288
|
+
Validates the template itself and reports any unknown filters used in `${...}` chains.
|
|
289
|
+
|
|
275
290
|
### `RuleTemplate.validateVariableNode(astNode, variableType)` (Static)
|
|
276
291
|
|
|
277
292
|
Helper method to validate that an AST node matches the expected variable type.
|
|
@@ -3888,11 +3888,13 @@ class RuleTemplate {
|
|
|
3888
3888
|
|
|
3889
3889
|
// Extract filters
|
|
3890
3890
|
const filters = [];
|
|
3891
|
+
const filterCalls = [];
|
|
3891
3892
|
for (const child of templateExpr.children || []) {
|
|
3892
3893
|
if (child.type === 'template_filter_call') {
|
|
3893
|
-
const
|
|
3894
|
-
if (
|
|
3895
|
-
filters.push(
|
|
3894
|
+
const filterCall = this._extractFilterCall(child);
|
|
3895
|
+
if (filterCall) {
|
|
3896
|
+
filters.push(filterCall.name);
|
|
3897
|
+
filterCalls.push(filterCall);
|
|
3896
3898
|
}
|
|
3897
3899
|
}
|
|
3898
3900
|
}
|
|
@@ -3901,38 +3903,128 @@ class RuleTemplate {
|
|
|
3901
3903
|
const start = node.start;
|
|
3902
3904
|
const end = node.end;
|
|
3903
3905
|
|
|
3904
|
-
return { name, filters, start, end };
|
|
3906
|
+
return { name, filters, filterCalls, start, end };
|
|
3905
3907
|
}
|
|
3906
3908
|
|
|
3907
3909
|
/**
|
|
3908
|
-
* Extract filter
|
|
3910
|
+
* Extract filter call from template_filter_call node
|
|
3909
3911
|
* @private
|
|
3910
3912
|
*/
|
|
3911
|
-
|
|
3913
|
+
_extractFilterCall(node) {
|
|
3912
3914
|
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
3913
3915
|
if (!filterNameNode || !filterNameNode.text) return null;
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
+
|
|
3917
|
+
const argsNode = node.children?.find(c => c.type === 'template_filter_args');
|
|
3918
|
+
|
|
3919
|
+
return {
|
|
3920
|
+
name: filterNameNode.text.trim(),
|
|
3921
|
+
args: this._extractFilterArgs(argsNode)
|
|
3922
|
+
};
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
_extractFilterArgs(node) {
|
|
3926
|
+
if (!node || !Array.isArray(node.children)) {
|
|
3927
|
+
return [];
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
return node.children
|
|
3931
|
+
.filter(child => child.type === 'template_filter_arg')
|
|
3932
|
+
.map(child => this._extractFilterArgValue(child));
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
_extractFilterArgValue(node) {
|
|
3936
|
+
if (!node || !Array.isArray(node.children) || node.children.length === 0) {
|
|
3937
|
+
return this._normalizeFilterArgText(node?.text?.trim() || '');
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
const child = node.children[0];
|
|
3941
|
+
if (!child) {
|
|
3942
|
+
return this._normalizeFilterArgText(node.text?.trim() || '');
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
if (child.type === 'value' && Array.isArray(child.children) && child.children.length > 0) {
|
|
3946
|
+
return this._extractFilterArgValue(child);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
if (child.type === 'string') {
|
|
3950
|
+
try {
|
|
3951
|
+
return JSON.parse(child.text);
|
|
3952
|
+
} catch (error) {
|
|
3953
|
+
return this._normalizeFilterArgText(child.text);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
if (child.type === 'number') {
|
|
3958
|
+
return Number(child.text);
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
if (child.type === 'true') {
|
|
3962
|
+
return true;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
if (child.type === 'false') {
|
|
3966
|
+
return false;
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
if (child.type === 'null') {
|
|
3970
|
+
return null;
|
|
3971
|
+
}
|
|
3972
|
+
|
|
3973
|
+
return this._normalizeFilterArgText(child.text?.trim() || node.text?.trim() || '');
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
_normalizeFilterArgText(text) {
|
|
3977
|
+
const normalizedText = String(text).trim();
|
|
3978
|
+
|
|
3979
|
+
if ((normalizedText.startsWith('"') && normalizedText.endsWith('"')) || (normalizedText.startsWith("'") && normalizedText.endsWith("'"))) {
|
|
3980
|
+
return normalizedText.slice(1, -1);
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
return normalizedText;
|
|
3916
3984
|
}
|
|
3917
3985
|
|
|
3918
3986
|
/**
|
|
3919
3987
|
* Validate variable types against the AST
|
|
3920
3988
|
* @param {Object} variables - Object mapping variable names to {type} objects
|
|
3921
|
-
* @
|
|
3989
|
+
* @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
|
|
3990
|
+
* @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
|
|
3922
3991
|
*/
|
|
3923
|
-
validate(variables) {
|
|
3992
|
+
validate(variables, functionBlob) {
|
|
3924
3993
|
if (!variables || typeof variables !== 'object') {
|
|
3925
3994
|
return {
|
|
3926
3995
|
valid: false,
|
|
3927
|
-
errors: ['Variables must be provided as an object']
|
|
3996
|
+
errors: ['Variables must be provided as an object'],
|
|
3997
|
+
warnings: []
|
|
3928
3998
|
};
|
|
3929
3999
|
}
|
|
3930
4000
|
|
|
3931
4001
|
const errors = [];
|
|
3932
|
-
const
|
|
4002
|
+
const warnings = [];
|
|
4003
|
+
const extractedVars = this._extractTemplateVariables();
|
|
4004
|
+
const seenVariables = new Set();
|
|
4005
|
+
const seenFilterErrors = new Set();
|
|
3933
4006
|
|
|
3934
4007
|
for (const varInfo of extractedVars) {
|
|
3935
4008
|
const varName = varInfo.name;
|
|
4009
|
+
|
|
4010
|
+
for (const filter of (varInfo.filterCalls || varInfo.filters || [])) {
|
|
4011
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
4012
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
4013
|
+
continue;
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${varName}'`;
|
|
4017
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
4018
|
+
errors.push(errorMessage);
|
|
4019
|
+
seenFilterErrors.add(errorMessage);
|
|
4020
|
+
}
|
|
4021
|
+
}
|
|
4022
|
+
|
|
4023
|
+
if (seenVariables.has(varName)) {
|
|
4024
|
+
continue;
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4027
|
+
seenVariables.add(varName);
|
|
3936
4028
|
|
|
3937
4029
|
// Check if variable is provided
|
|
3938
4030
|
if (!variables.hasOwnProperty(varName)) {
|
|
@@ -3961,13 +4053,74 @@ class RuleTemplate {
|
|
|
3961
4053
|
}
|
|
3962
4054
|
}
|
|
3963
4055
|
}
|
|
4056
|
+
|
|
4057
|
+
if (functionBlob && typeof functionBlob.validate === 'function') {
|
|
4058
|
+
for (const functionCall of this._extractFunctionCalls()) {
|
|
4059
|
+
warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
3964
4062
|
|
|
3965
4063
|
return {
|
|
3966
4064
|
valid: errors.length === 0,
|
|
3967
|
-
errors
|
|
4065
|
+
errors,
|
|
4066
|
+
warnings
|
|
3968
4067
|
};
|
|
3969
4068
|
}
|
|
3970
4069
|
|
|
4070
|
+
_extractFunctionCalls() {
|
|
4071
|
+
const functionCalls = [];
|
|
4072
|
+
|
|
4073
|
+
const traverse = (node) => {
|
|
4074
|
+
if (!node) return;
|
|
4075
|
+
|
|
4076
|
+
if (node.type === 'fcall') {
|
|
4077
|
+
const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
|
|
4078
|
+
const argumentsNode = node.children?.find(c => c.type === 'arguments');
|
|
4079
|
+
if (functionName) {
|
|
4080
|
+
functionCalls.push({
|
|
4081
|
+
name: functionName,
|
|
4082
|
+
arguments: argumentsNode?.children
|
|
4083
|
+
?.filter(c => c.type === 'argument')
|
|
4084
|
+
.map(c => c.text) || []
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
if (node.children) {
|
|
4090
|
+
for (const child of node.children) {
|
|
4091
|
+
traverse(child);
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
|
|
4096
|
+
traverse(this.ast);
|
|
4097
|
+
return functionCalls;
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
_extractTemplateVariables() {
|
|
4101
|
+
const variables = [];
|
|
4102
|
+
|
|
4103
|
+
const traverse = (node) => {
|
|
4104
|
+
if (!node) return;
|
|
4105
|
+
|
|
4106
|
+
if (node.type === 'template_value') {
|
|
4107
|
+
const variableInfo = this._extractVariableFromNode(node);
|
|
4108
|
+
if (variableInfo) {
|
|
4109
|
+
variables.push(variableInfo);
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
if (node.children) {
|
|
4114
|
+
for (const child of node.children) {
|
|
4115
|
+
traverse(child);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
|
|
4120
|
+
traverse(this.ast);
|
|
4121
|
+
return variables;
|
|
4122
|
+
}
|
|
4123
|
+
|
|
3971
4124
|
/**
|
|
3972
4125
|
* Prepare the template by replacing variables with their values
|
|
3973
4126
|
* Rebuilds from AST by iterating through children
|
|
@@ -4070,12 +4223,15 @@ class RuleTemplate {
|
|
|
4070
4223
|
|
|
4071
4224
|
// Apply filters if present
|
|
4072
4225
|
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
4073
|
-
for (const
|
|
4074
|
-
|
|
4075
|
-
|
|
4226
|
+
for (const filter of (templateInfo.filterCalls || templateInfo.filters)) {
|
|
4227
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
4228
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
4229
|
+
|
|
4230
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
4231
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
4076
4232
|
}
|
|
4077
|
-
|
|
4078
|
-
TemplateFilters[filterName](varData);
|
|
4233
|
+
|
|
4234
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
4079
4235
|
}
|
|
4080
4236
|
}
|
|
4081
4237
|
|
|
@@ -4153,6 +4309,31 @@ module.exports = RuleTemplate;
|
|
|
4153
4309
|
Template filters are functions that transform variable values.
|
|
4154
4310
|
They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
|
|
4155
4311
|
*/
|
|
4312
|
+
const HUMANISE_TIME_UNITS = [
|
|
4313
|
+
{ name: 'year', seconds: 31536000, aliases: ['year', 'years', 'yr', 'yrs', 'y'] },
|
|
4314
|
+
{ name: 'month', seconds: 2592000, aliases: ['month', 'months', 'mo', 'mos'] },
|
|
4315
|
+
{ name: 'week', seconds: 604800, aliases: ['week', 'weeks', 'wk', 'wks', 'w'] },
|
|
4316
|
+
{ name: 'day', seconds: 86400, aliases: ['day', 'days', 'd'] },
|
|
4317
|
+
{ name: 'hour', seconds: 3600, aliases: ['hour', 'hours', 'hr', 'hrs', 'h'] },
|
|
4318
|
+
{ name: 'minute', seconds: 60, aliases: ['minute', 'minutes', 'min', 'mins'] },
|
|
4319
|
+
{ name: 'second', seconds: 1, aliases: ['second', 'seconds', 'sec', 'secs', 's'] }
|
|
4320
|
+
];
|
|
4321
|
+
|
|
4322
|
+
const getHumaniseTimeUnit = minUnit => {
|
|
4323
|
+
if (minUnit === null || minUnit === undefined || minUnit === '') {
|
|
4324
|
+
return null;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
const normalizedMinUnit = String(minUnit).trim().toLowerCase();
|
|
4328
|
+
const unit = HUMANISE_TIME_UNITS.find(candidate => candidate.aliases.includes(normalizedMinUnit));
|
|
4329
|
+
|
|
4330
|
+
if (!unit) {
|
|
4331
|
+
throw new Error(`Unknown humanise_time min_unit \"${minUnit}\"`);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
return unit;
|
|
4335
|
+
};
|
|
4336
|
+
|
|
4156
4337
|
const TemplateFilters = {
|
|
4157
4338
|
// Convert value to JSON string representation
|
|
4158
4339
|
string: varData => {
|
|
@@ -4276,6 +4457,69 @@ const TemplateFilters = {
|
|
|
4276
4457
|
|
|
4277
4458
|
},
|
|
4278
4459
|
|
|
4460
|
+
humanise_list: (varData, joiner = 'and') => {
|
|
4461
|
+
if (typeof varData.value === 'string') {
|
|
4462
|
+
varData.type = 'string';
|
|
4463
|
+
return;
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
if (!Array.isArray(varData.value)) {
|
|
4467
|
+
varData.value = String(varData.value);
|
|
4468
|
+
varData.type = 'string';
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
const items = varData.value.map(item => String(item));
|
|
4473
|
+
|
|
4474
|
+
if (items.length === 0) {
|
|
4475
|
+
varData.value = '';
|
|
4476
|
+
} else if (items.length === 1) {
|
|
4477
|
+
[varData.value] = items;
|
|
4478
|
+
} else if (items.length === 2) {
|
|
4479
|
+
varData.value = `${items[0]} ${joiner} ${items[1]}`;
|
|
4480
|
+
} else {
|
|
4481
|
+
varData.value = `${items.slice(0, -1).join(', ')} ${joiner} ${items[items.length - 1]}`;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
varData.type = 'string';
|
|
4485
|
+
|
|
4486
|
+
},
|
|
4487
|
+
|
|
4488
|
+
humanise_time: (varData, minUnit = null) => {
|
|
4489
|
+
const rawSeconds = Number(varData.value);
|
|
4490
|
+
|
|
4491
|
+
if (isNaN(rawSeconds)) {
|
|
4492
|
+
throw new Error(`Value "${varData.value}" cannot be converted to seconds`);
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
const isNegative = rawSeconds < 0;
|
|
4496
|
+
const absoluteSeconds = Math.abs(rawSeconds);
|
|
4497
|
+
const minimumUnit = getHumaniseTimeUnit(minUnit);
|
|
4498
|
+
const minimumUnitIndex = minimumUnit
|
|
4499
|
+
? HUMANISE_TIME_UNITS.findIndex(unit => unit.name === minimumUnit.name)
|
|
4500
|
+
: HUMANISE_TIME_UNITS.length - 1;
|
|
4501
|
+
const candidateUnits = HUMANISE_TIME_UNITS.slice(0, minimumUnitIndex + 1);
|
|
4502
|
+
let selectedUnit = candidateUnits.find(unit => absoluteSeconds % unit.seconds === 0);
|
|
4503
|
+
let quantity;
|
|
4504
|
+
|
|
4505
|
+
if (selectedUnit) {
|
|
4506
|
+
quantity = absoluteSeconds / selectedUnit.seconds;
|
|
4507
|
+
} else if (minimumUnit) {
|
|
4508
|
+
selectedUnit = minimumUnit;
|
|
4509
|
+
quantity = Math.floor(absoluteSeconds / selectedUnit.seconds);
|
|
4510
|
+
} else {
|
|
4511
|
+
selectedUnit = HUMANISE_TIME_UNITS[HUMANISE_TIME_UNITS.length - 1];
|
|
4512
|
+
quantity = absoluteSeconds;
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
const signedQuantity = isNegative ? -quantity : quantity;
|
|
4516
|
+
const label = Math.abs(signedQuantity) === 1 ? selectedUnit.name : `${selectedUnit.name}s`;
|
|
4517
|
+
|
|
4518
|
+
varData.value = `${signedQuantity} ${label}`;
|
|
4519
|
+
varData.type = 'string';
|
|
4520
|
+
|
|
4521
|
+
},
|
|
4522
|
+
|
|
4279
4523
|
// Extract start time from time period/time period ago as time value
|
|
4280
4524
|
time_start: varData => {
|
|
4281
4525
|
if (varData.type === 'time period' || varData.type === 'time period ago') {
|
|
@@ -4450,12 +4694,15 @@ class VariableValidate {
|
|
|
4450
4694
|
throw new Error('Variable data filters must be an array');
|
|
4451
4695
|
}
|
|
4452
4696
|
|
|
4453
|
-
for (const
|
|
4454
|
-
|
|
4455
|
-
|
|
4697
|
+
for (const filter of normalizedVarData.filters) {
|
|
4698
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
4699
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
4700
|
+
|
|
4701
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
4702
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
4456
4703
|
}
|
|
4457
4704
|
|
|
4458
|
-
TemplateFilters[filterName](normalizedVarData);
|
|
4705
|
+
TemplateFilters[filterName](normalizedVarData, ...filterArgs);
|
|
4459
4706
|
}
|
|
4460
4707
|
|
|
4461
4708
|
return normalizedVarData;
|
package/index.d.ts
CHANGED
|
@@ -3,9 +3,14 @@ export interface VariablePosition {
|
|
|
3
3
|
end: number;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export interface TemplateFilterCall {
|
|
7
|
+
name: string;
|
|
8
|
+
args: any[];
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
export interface VariableInfo {
|
|
7
12
|
name: string;
|
|
8
|
-
filters: string
|
|
13
|
+
filters: Array<string | TemplateFilterCall>;
|
|
9
14
|
positions: VariablePosition[];
|
|
10
15
|
}
|
|
11
16
|
|
|
@@ -26,6 +31,7 @@ export interface Variables {
|
|
|
26
31
|
export interface ValidationResult {
|
|
27
32
|
valid: boolean;
|
|
28
33
|
errors: string[];
|
|
34
|
+
warnings: string[];
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
export interface VariableValidationResult {
|
|
@@ -60,6 +66,17 @@ export interface TemplateFiltersType {
|
|
|
60
66
|
[key: string]: FilterFunction;
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
export interface HalleyFunctionDefinition {
|
|
70
|
+
name: string;
|
|
71
|
+
arguments: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface HalleyFunctionBlobData {
|
|
75
|
+
_schema?: number;
|
|
76
|
+
version?: string;
|
|
77
|
+
functions?: HalleyFunctionDefinition[];
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
export class RuleTemplate {
|
|
64
81
|
ruleTemplateText: string;
|
|
65
82
|
ast: ASTNode;
|
|
@@ -90,7 +107,7 @@ export class RuleTemplate {
|
|
|
90
107
|
* @param variables Object mapping variable names to {value, type} objects
|
|
91
108
|
* @returns Object with validation results: {valid, errors}
|
|
92
109
|
*/
|
|
93
|
-
validate(variables: Variables): ValidationResult;
|
|
110
|
+
validate(variables: Variables, functionBlob?: HalleyFunctionBlob): ValidationResult;
|
|
94
111
|
|
|
95
112
|
/**
|
|
96
113
|
* Prepare the template by replacing variables with their values
|
|
@@ -122,9 +139,23 @@ export class GeneralTemplate {
|
|
|
122
139
|
|
|
123
140
|
extractVariables(): VariableInfo[];
|
|
124
141
|
|
|
142
|
+
validate(): ValidationResult;
|
|
143
|
+
|
|
125
144
|
prepare(variables: Variables): string;
|
|
126
145
|
}
|
|
127
146
|
|
|
147
|
+
export class HalleyFunctionBlob {
|
|
148
|
+
_schema?: number;
|
|
149
|
+
version?: string;
|
|
150
|
+
functions: HalleyFunctionDefinition[];
|
|
151
|
+
|
|
152
|
+
constructor(jsonData: HalleyFunctionBlobData);
|
|
153
|
+
|
|
154
|
+
static fromURL(url: string): Promise<HalleyFunctionBlob>;
|
|
155
|
+
|
|
156
|
+
validate(functionName: string, variables?: any[]): string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
128
159
|
export class VariableTemplate {
|
|
129
160
|
templateText: string;
|
|
130
161
|
ast: ASTNode;
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const RuleTemplate = require('./src/RuleTemplate');
|
|
2
2
|
const GeneralTemplate = require('./src/GeneralTemplate');
|
|
3
|
+
const HalleyFunctionBlob = require('./src/HalleyFunctionBlob');
|
|
3
4
|
const VariableTemplate = require('./src/VariableTemplate');
|
|
4
5
|
const VariableValidate = require('./src/VariableValidate');
|
|
5
6
|
|
|
@@ -9,4 +10,5 @@ module.exports.VariableTypes = RuleTemplate.VariableTypes;
|
|
|
9
10
|
module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
|
|
10
11
|
module.exports.VariableValidate = VariableValidate;
|
|
11
12
|
module.exports.GeneralTemplate = GeneralTemplate;
|
|
13
|
+
module.exports.HalleyFunctionBlob = HalleyFunctionBlob;
|
|
12
14
|
module.exports.VariableTemplate = VariableTemplate;
|
package/package.json
CHANGED
package/src/GeneralTemplate.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const TemplateFilters = require('./TemplateFilters');
|
|
2
2
|
|
|
3
|
+
const FILTER_PATTERN = /([A-Za-z_][A-Za-z0-9_]*)(?:\((.*)\))?$/;
|
|
4
|
+
|
|
3
5
|
class GeneralTemplate {
|
|
4
6
|
constructor(templateText) {
|
|
5
7
|
this.templateText = templateText;
|
|
@@ -52,6 +54,33 @@ class GeneralTemplate {
|
|
|
52
54
|
return this.getVariables();
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
validate() {
|
|
58
|
+
const errors = [];
|
|
59
|
+
const warnings = [];
|
|
60
|
+
const seenFilterErrors = new Set();
|
|
61
|
+
|
|
62
|
+
for (const variableInfo of this.getVariables()) {
|
|
63
|
+
for (const filter of variableInfo.filters || []) {
|
|
64
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
65
|
+
if (filterName && TemplateFilters[filterName]) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const errorMessage = `Unknown filter '${filterName || filter}' for variable '${variableInfo.name}'`;
|
|
70
|
+
if (!seenFilterErrors.has(errorMessage)) {
|
|
71
|
+
errors.push(errorMessage);
|
|
72
|
+
seenFilterErrors.add(errorMessage);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
valid: errors.length === 0,
|
|
79
|
+
errors,
|
|
80
|
+
warnings
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
prepare(variables) {
|
|
56
85
|
if (!variables || typeof variables !== 'object') {
|
|
57
86
|
throw new Error('Variables must be provided as an object');
|
|
@@ -76,12 +105,15 @@ class GeneralTemplate {
|
|
|
76
105
|
varData = Object.assign({}, varData);
|
|
77
106
|
|
|
78
107
|
if (parsedExpression.filters && parsedExpression.filters.length > 0) {
|
|
79
|
-
for (const
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
for (const filter of parsedExpression.filters) {
|
|
109
|
+
const filterName = typeof filter === 'string' ? filter : filter?.name;
|
|
110
|
+
const filterArgs = typeof filter === 'string' ? [] : (Array.isArray(filter?.args) ? filter.args : []);
|
|
111
|
+
|
|
112
|
+
if (!filterName || !TemplateFilters[filterName]) {
|
|
113
|
+
throw new Error(`Unknown filter '${filterName || filter}'`);
|
|
82
114
|
}
|
|
83
115
|
|
|
84
|
-
TemplateFilters[filterName](varData);
|
|
116
|
+
TemplateFilters[filterName](varData, ...filterArgs);
|
|
85
117
|
}
|
|
86
118
|
}
|
|
87
119
|
|
|
@@ -101,10 +133,58 @@ class GeneralTemplate {
|
|
|
101
133
|
|
|
102
134
|
return {
|
|
103
135
|
name: segments[0],
|
|
104
|
-
filters: segments.slice(1)
|
|
136
|
+
filters: segments.slice(1).map(segment => this._parseFilter(segment))
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
_parseFilter(segment) {
|
|
141
|
+
const match = segment.match(FILTER_PATTERN);
|
|
142
|
+
if (!match) {
|
|
143
|
+
return {
|
|
144
|
+
name: segment,
|
|
145
|
+
args: []
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const [, name, rawArgs] = match;
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
args: this._parseFilterArgs(rawArgs)
|
|
105
153
|
};
|
|
106
154
|
}
|
|
107
155
|
|
|
156
|
+
_parseFilterArgs(rawArgs) {
|
|
157
|
+
if (!rawArgs || !rawArgs.trim()) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return rawArgs.split(',').map(arg => this._parseFilterArgValue(arg.trim()));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_parseFilterArgValue(rawArg) {
|
|
165
|
+
if ((rawArg.startsWith('"') && rawArg.endsWith('"')) || (rawArg.startsWith("'") && rawArg.endsWith("'"))) {
|
|
166
|
+
return rawArg.slice(1, -1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (rawArg === 'true') {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (rawArg === 'false') {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (rawArg === 'null') {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (rawArg !== '' && !Number.isNaN(Number(rawArg))) {
|
|
182
|
+
return Number(rawArg);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return rawArg;
|
|
186
|
+
}
|
|
187
|
+
|
|
108
188
|
_serializeVariable(varData) {
|
|
109
189
|
if (varData.value === null || varData.value === undefined) {
|
|
110
190
|
return '';
|