@halleyassist/rule-templater 0.0.12 → 0.0.14
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 +89 -0
- package/dist/rule-templater.browser.js +11 -7
- package/index.d.ts +20 -0
- package/index.js +3 -1
- package/package.json +2 -2
- package/src/RuleTemplate.browser.js +1 -0
- package/src/{RuleTemplater.js → RuleTemplate.js} +2 -0
- package/src/RuleTemplate.production.js +427 -0
- package/src/RuleTemplater.browser.js +1 -1
- package/src/RuleTemplater.production.js +24 -42
- package/src/TemplateFilters.js +3 -1
- package/src/VariableTemplate.js +126 -0
package/README.md
CHANGED
|
@@ -167,6 +167,26 @@ const prepared = parsed.prepare({
|
|
|
167
167
|
// If a door is opened between 08:00 AND 12:00
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
### Variable Template
|
|
171
|
+
|
|
172
|
+
For a single variable expression, use `VariableTemplate`:
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
const { VariableTemplate } = require('@halleyassist/rule-templater');
|
|
176
|
+
|
|
177
|
+
const parsed = VariableTemplate.parse('ALERT_PERIOD|time_start|upper');
|
|
178
|
+
const variable = parsed.extractVariable();
|
|
179
|
+
// { name: 'ALERT_PERIOD', filters: ['time_start', 'upper'] }
|
|
180
|
+
|
|
181
|
+
const formatted = parsed.format({
|
|
182
|
+
ALERT_PERIOD: {
|
|
183
|
+
value: { from: '08:00', to: '12:00' },
|
|
184
|
+
type: 'time period'
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// { value: '08:00', type: 'string' }
|
|
188
|
+
```
|
|
189
|
+
|
|
170
190
|
## API
|
|
171
191
|
|
|
172
192
|
### `RuleTemplate.parse(ruleTemplate)`
|
|
@@ -301,6 +321,75 @@ The following variable types are supported:
|
|
|
301
321
|
- `boolean array`
|
|
302
322
|
- `object array`
|
|
303
323
|
|
|
324
|
+
### Time Type Formats
|
|
325
|
+
|
|
326
|
+
The time-related variable types use the following structures when passed to `prepare()` or `validate()`:
|
|
327
|
+
|
|
328
|
+
#### `time value`
|
|
329
|
+
|
|
330
|
+
Use a time-of-day string such as `08:00`.
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
{
|
|
334
|
+
value: '08:00',
|
|
335
|
+
type: 'time value'
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
This is useful when a rule expects a single time value, for example:
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
const prepared = parsed.prepare({
|
|
343
|
+
START_TIME: { value: '08:00', type: 'time value' }
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### `time period`
|
|
348
|
+
|
|
349
|
+
Use an object with `from` and `to` properties, both using the same time-of-day string format as `time value`.
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
{
|
|
353
|
+
value: {
|
|
354
|
+
from: '08:00',
|
|
355
|
+
to: '12:00'
|
|
356
|
+
},
|
|
357
|
+
type: 'time period'
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
When rendered into a rule template, this becomes:
|
|
362
|
+
|
|
363
|
+
```text
|
|
364
|
+
08:00 TO 12:00
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### `time period ago`
|
|
368
|
+
|
|
369
|
+
Use the same structure as `time period`, plus an `ago` tuple containing:
|
|
370
|
+
|
|
371
|
+
- the numeric offset
|
|
372
|
+
- the rule time unit token, for example `HOURS`
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
{
|
|
376
|
+
value: {
|
|
377
|
+
from: '08:00',
|
|
378
|
+
to: '12:00',
|
|
379
|
+
ago: [2, 'HOURS']
|
|
380
|
+
},
|
|
381
|
+
type: 'time period ago'
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
When rendered into a rule template, this becomes:
|
|
386
|
+
|
|
387
|
+
```text
|
|
388
|
+
08:00 TO 12:00 AGO 2 HOURS
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The `time_start` and `time_end` filters can extract `from` and `to` from either `time period` or `time period ago` and convert them to `time value`.
|
|
392
|
+
|
|
304
393
|
## License
|
|
305
394
|
|
|
306
395
|
ISC
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.
|
|
1
|
+
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.RuleTemplate = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
|
|
2
2
|
module.exports=[{"name":"statement_main","bnf":[["statement","EOF"]]},{"name":"logical_operator","bnf":[["AND"],["OR"]]},{"name":"%statement[2]","bnf":[["logical_operator","expression"]],"fragment":true},{"name":"statement","bnf":[["expression","%statement[2]*"]]},{"name":"expression","bnf":[["not_expression"],["standard_expression"],["parenthesis_expression"]]},{"name":"parenthesis_expression","bnf":[["BEGIN_PARENTHESIS","WS*","statement","WS*","END_PARENTHESIS"]]},{"name":"%not_expression[2]","bnf":[["result"],["parenthesis_expression"]],"fragment":true},{"name":"not_expression","bnf":[["NOT","%not_expression[2]"]]},{"name":"%standard_expression[2][1]","bnf":[["WS*","eq_approx"]],"fragment":true},{"name":"%standard_expression[2][2]","bnf":[["WS*","basic_rhs"]],"fragment":true},{"name":"%standard_expression[2][3][1]","bnf":[["WS+","IS"]],"fragment":true},{"name":"%standard_expression[2][3]","bnf":[["%standard_expression[2][3][1]?","WS+","between"]],"fragment":true},{"name":"%standard_expression[2][4]","bnf":[["WS+","in_expr"]],"fragment":true},{"name":"%standard_expression[2]","bnf":[["%standard_expression[2][1]"],["%standard_expression[2][2]"],["%standard_expression[2][3]"],["%standard_expression[2][4]"]],"fragment":true},{"name":"standard_expression","bnf":[["result","%standard_expression[2]?"]]},{"name":"basic_rhs","bnf":[["operator","WS*","result"]]},{"name":"eq_approx","bnf":[["eq_operator","WS*","\"~\"","WS*","result"]]},{"name":"PLUS","bnf":[["\"+\""]]},{"name":"MINUS","bnf":[["\"-\""]]},{"name":"MULTIPLY","bnf":[["\"*\""]]},{"name":"DIVIDE","bnf":[["\"/\""]]},{"name":"MODULUS","bnf":[["\"%\""]]},{"name":"DEFAULT_VAL","bnf":[["\"??\""]]},{"name":"arithmetic_operator","bnf":[["PLUS"],["MINUS"],["MULTIPLY"],["DIVIDE"],["MODULUS"],["DEFAULT_VAL"]]},{"name":"number_atom","bnf":[["number"]]},{"name":"number_time_atom","bnf":[["number_time"]]},{"name":"tod_atom","bnf":[["number_tod"]]},{"name":"dow_atom","bnf":[["dow"]]},{"name":"arithmetic_operand","bnf":[["fcall"],["number_time_atom"],["number_atom"]]},{"name":"%arithmetic_result[5]","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","WS*","arithmetic_operator","WS*","%arithmetic_result[5]"]]},{"name":"simple_result","bnf":[["fcall"],["value"]]},{"name":"result","bnf":[["arithmetic_result"],["simple_result"]]},{"name":"value_atom","bnf":[["false"],["true"],["array"],["time_period"],["number_time_atom"],["number_atom"],["tod_atom"],["string"]]},{"name":"value","bnf":[["value_atom"]]},{"name":"BEGIN_ARRAY","bnf":[["WS*",/\x5B/,"WS*"]]},{"name":"BEGIN_OBJECT","bnf":[["WS*",/\x7B/,"WS*"]]},{"name":"END_ARRAY","bnf":[["WS*",/\x5D/,"WS*"]]},{"name":"END_OBJECT","bnf":[["WS*",/\x7D/,"WS*"]]},{"name":"NAME_SEPARATOR","bnf":[["WS*",/\x3A/,"WS*"]]},{"name":"VALUE_SEPARATOR","bnf":[["WS*",/\x2C/,"WS*"]]},{"name":"WS","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"name":"operator","bnf":[["GTE"],["LTE"],["GT"],["LT"],["EQ"],["NEQ"]]},{"name":"eq_operator","bnf":[["EQ"],["NEQ"]]},{"name":"BEGIN_ARGUMENT","bnf":[["\"(\""]]},{"name":"END_ARGUMENT","bnf":[["\")\""]]},{"name":"BEGIN_PARENTHESIS","bnf":[["\"(\""]]},{"name":"END_PARENTHESIS","bnf":[["\")\""]]},{"name":"BEGIN_IN","bnf":[[/[Ii]/,/[Nn]/]]},{"name":"in_expr","bnf":[["BEGIN_IN","WS*","BEGIN_PARENTHESIS","WS*","arguments","END_PARENTHESIS"]]},{"name":"argument","bnf":[["statement","WS*"]]},{"name":"%arguments[2]","bnf":[["WS*","\",\"","WS*","argument"]],"fragment":true},{"name":"arguments","bnf":[["argument","%arguments[2]*"]]},{"name":"%fname[1]","bnf":[[/[A-Za-z0-9]/]]},{"name":"fname","bnf":[["%fname[1]+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_dash_or_and[1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_dash_or_and[2]","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"between_dash_or_and","bnf":[["%between_dash_or_and[1]"],["%between_dash_or_and[2]"]]},{"name":"between_number_inner","bnf":[["number_atom"],["number_time_atom"]]},{"name":"between_number","bnf":[["between_number_inner","between_dash_or_and","between_number_inner"]]},{"name":"between_number_time_inner","bnf":[["number_time_atom"]]},{"name":"%between_number_time[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_number_time","bnf":[["between_number_time_inner","between_dash_or_and","between_number_time_inner","%between_number_time[4]?"]]},{"name":"between_tod_inner","bnf":[["tod_atom"]]},{"name":"%between_tod[2]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["between_tod_inner","%between_tod[2]","between_tod_inner","%between_tod[4]?"]]},{"name":"%between[3]","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between[3]"]]},{"name":"dow","bnf":[[/[Mm]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Oo]/,/[Nn]/],[/[Tt]/,/[Uu]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Uu]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Dd]/,/[Nn]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ww]/,/[Ee]/,/[Dd]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Hh]/,/[Uu]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/],[/[Ff]/,/[Rr]/,/[Ii]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ff]/,/[Rr]/,/[Ii]/],[/[Ss]/,/[Aa]/,/[Tt]/,/[Uu]/,/[Rr]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Aa]/,/[Tt]/],[/[Ss]/,/[Uu]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Uu]/,/[Nn]/]]},{"name":"dow_range_inner","bnf":[["dow_atom"]]},{"name":"%dow_range[4]","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow_range_inner"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow_range_inner","%dow_range[4]?"]]},{"name":"between_time_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_number_time"]]},{"name":"between_tod_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_tod"]]},{"name":"between_time_only_atom","bnf":[["between_time_only"]]},{"name":"between_tod_only_atom","bnf":[["between_tod_only"]]},{"name":"%AND[1]","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND[2]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND[1]"],["%AND[2]"]]},{"name":"%OR[1]","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR[2]","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR[1]"],["%OR[2]"]]},{"name":"AGO","bnf":[[/[Aa]/,/[Gg]/,/[Oo]/]]},{"name":"GT","bnf":[["\">\""]]},{"name":"LT","bnf":[["\"<\""]]},{"name":"GTE","bnf":[["\">=\""]]},{"name":"LTE","bnf":[["\"<=\""]]},{"name":"IS","bnf":[[/[Ii]/,/[Ss]/]]},{"name":"EQ","bnf":[["\"==\""],["\"=\""]]},{"name":"NEQ","bnf":[["\"!=\""]]},{"name":"%NOT[1]","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT[2]","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT[1]"],["%NOT[2]"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%array[2][2]","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array[2]","bnf":[["value","%array[2][2]*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array[2]?","END_ARRAY"]]},{"name":"unit","bnf":[[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/,/[Ss]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/,/[Ss]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/,/[Ss]/],[/[Dd]/,/[Aa]/,/[Yy]/,/[Ss]/],[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/],[/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/]]},{"name":"%number[2][1]","bnf":[[/[0-9]/]]},{"name":"%number[2]","bnf":[["%number[2][1]+"]],"fragment":true},{"name":"%number[3][2]","bnf":[[/[0-9]/]]},{"name":"%number[3]","bnf":[["\".\"","%number[3][2]+"]],"fragment":true},{"name":"%number[4][2]","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%number[4][3][2]","bnf":[[/[0-9]/]]},{"name":"%number[4][3]","bnf":[["\"0\""],[/[1-9]/,"%number[4][3][2]*"]],"fragment":true},{"name":"%number[4]","bnf":[["\"e\"","%number[4][2]?","%number[4][3]"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number[2]","%number[3]?","%number[4]?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%number_tod[1][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[1]","bnf":[["%number_tod[1][1]+"]],"fragment":true},{"name":"%number_tod[3][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[3]","bnf":[["%number_tod[3][1]+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod[1]","\":\"","%number_tod[3]"]]},{"name":"%time_period_ago[2]","bnf":[["WS+","number_time_atom"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time_atom","%time_period_ago[2]*","WS+","AGO"]]},{"name":"%time_period_ago_between[2]","bnf":[["WS+","number_time_atom"]],"fragment":true},{"name":"%time_period_ago_between[6]","bnf":[["between_time_only_atom"],["between_tod_only_atom"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time_atom","%time_period_ago_between[2]*","WS+","AGO","WS+","%time_period_ago_between[6]"]]},{"name":"time_period_const","bnf":[[/[Tt]/,/[Oo]/,/[Dd]/,/[Aa]/,/[Yy]/],["time_period_ago"]]},{"name":"time_period","bnf":[["time_period_ago_between"],["time_period_const"],["between_tod_only"],["between_time_only"]]},{"name":"%string[2][1]","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%string[2][2]","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string[2]","bnf":[["%string[2][1]"],[/\x5C/,"%string[2][2]"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string[2]*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
|
|
3
3
|
},{}],2:[function(require,module,exports){
|
|
4
4
|
const {Parser} = require('ebnf/dist/Parser.js'), {ParsingError} = require('ebnf'), RuleParseError = require('./errors/RuleParseError');
|
|
@@ -3644,10 +3644,10 @@ Object.defineProperty(exports, "ParsingError", { enumerable: true, get: function
|
|
|
3644
3644
|
exports.Grammars = require("./Grammars");
|
|
3645
3645
|
|
|
3646
3646
|
},{"./Grammars":8,"./Parser":9,"./ParsingError":10,"./TokenError":12}],14:[function(require,module,exports){
|
|
3647
|
+
module.exports = require('./RuleTemplate.production.js');
|
|
3648
|
+
},{"./RuleTemplate.production.js":16}],15:[function(require,module,exports){
|
|
3647
3649
|
module.exports=[{"name":"TEMPLATE_BEGIN","bnf":[["\"${\""]]},{"name":"TEMPLATE_END","bnf":[["\"}\""]]},{"name":"PIPE","bnf":[["\"|\""]]},{"name":"%IDENT[2]","bnf":[[/[A-Za-z0-9_]/]]},{"name":"IDENT","bnf":[[/[A-Za-z_]/,"%IDENT[2]*"]]},{"name":"DOT","bnf":[["\".\""]]},{"name":"template_value","bnf":[["TEMPLATE_BEGIN","WS*","template_expr","WS*","TEMPLATE_END"]]},{"name":"%template_expr[2]","bnf":[["WS*","template_pipe","WS*","template_filter_call"]],"fragment":true},{"name":"template_expr","bnf":[["template_path","%template_expr[2]*"]]},{"name":"template_pipe","bnf":[["PIPE"]]},{"name":"%template_path[2]","bnf":[["WS*","DOT","WS*","IDENT"]],"fragment":true},{"name":"template_path","bnf":[["IDENT","%template_path[2]*"]]},{"name":"%template_filter_call[2]","bnf":[["WS*","BEGIN_ARGUMENT","WS*","template_filter_args?","WS*","END_ARGUMENT"]],"fragment":true},{"name":"template_filter_call","bnf":[["template_filter_name","%template_filter_call[2]?"]]},{"name":"template_filter_name","bnf":[["IDENT"]]},{"name":"%template_filter_args[2]","bnf":[["WS*","\",\"","WS*","template_filter_arg"]],"fragment":true},{"name":"template_filter_args","bnf":[["template_filter_arg","%template_filter_args[2]*"]]},{"name":"template_filter_arg","bnf":[["value"],["template_value"]]},{"name":"number_atom","bnf":[["number"],["template_value"]]},{"name":"number_time_atom","bnf":[["number_time"],["template_value","WS+","unit"],["template_value"]]},{"name":"tod_atom","bnf":[["number_tod"],["template_value"]]},{"name":"dow_atom","bnf":[["dow"],["template_value"]]},{"name":"between_time_only_atom","bnf":[["between_time_only"],["template_value"]]},{"name":"between_tod_only_atom","bnf":[["between_tod_only"],["template_value"]]}]
|
|
3648
|
-
},{}],
|
|
3649
|
-
module.exports = require('./RuleTemplater.production.js');
|
|
3650
|
-
},{"./RuleTemplater.production.js":16}],16:[function(require,module,exports){
|
|
3650
|
+
},{}],16:[function(require,module,exports){
|
|
3651
3651
|
// Note: We are coupled closely with the ebnf grammar structure of rule-parser
|
|
3652
3652
|
const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
|
|
3653
3653
|
TemplateFilters = require('./TemplateFilters'),
|
|
@@ -3665,6 +3665,7 @@ const VariableTypes = [
|
|
|
3665
3665
|
'time period',
|
|
3666
3666
|
'time period ago',
|
|
3667
3667
|
'time value',
|
|
3668
|
+
'number time',
|
|
3668
3669
|
'string array',
|
|
3669
3670
|
'number array',
|
|
3670
3671
|
'boolean array',
|
|
@@ -3678,6 +3679,7 @@ const AllowedTypeMapping = {
|
|
|
3678
3679
|
'time period': ['time_period_atom'],
|
|
3679
3680
|
'time period ago': ['time_period_atom'],
|
|
3680
3681
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
3682
|
+
'number time': ['number_atom'],
|
|
3681
3683
|
'string array': ['string_array'],
|
|
3682
3684
|
'number array': ['number_array'],
|
|
3683
3685
|
'boolean array': ['boolean_array'],
|
|
@@ -4074,7 +4076,7 @@ RuleTemplate.TemplateFilters = TemplateFilters;
|
|
|
4074
4076
|
|
|
4075
4077
|
module.exports = RuleTemplate;
|
|
4076
4078
|
|
|
4077
|
-
},{"./RuleTemplate.production.ebnf.js":
|
|
4079
|
+
},{"./RuleTemplate.production.ebnf.js":15,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
|
|
4078
4080
|
/*
|
|
4079
4081
|
Template filters are functions that transform variable values.
|
|
4080
4082
|
They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
|
|
@@ -4129,7 +4131,9 @@ const TemplateFilters = {
|
|
|
4129
4131
|
number: varData => {
|
|
4130
4132
|
varData.value = Number(varData.value);
|
|
4131
4133
|
varData.type = 'number';
|
|
4132
|
-
|
|
4134
|
+
if(isNaN(varData.value)){
|
|
4135
|
+
throw new Error(`Value "${varData.value}" cannot be converted to a number`);
|
|
4136
|
+
}
|
|
4133
4137
|
},
|
|
4134
4138
|
|
|
4135
4139
|
// Convert to boolean
|
|
@@ -4232,5 +4236,5 @@ const TemplateFilters = {
|
|
|
4232
4236
|
}
|
|
4233
4237
|
|
|
4234
4238
|
module.exports = TemplateFilters;
|
|
4235
|
-
},{}]},{},[
|
|
4239
|
+
},{}]},{},[14])(14)
|
|
4236
4240
|
});
|
package/index.d.ts
CHANGED
|
@@ -119,6 +119,26 @@ export class GeneralTemplate {
|
|
|
119
119
|
prepare(variables: Variables): string;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
export class VariableTemplate {
|
|
123
|
+
templateText: string;
|
|
124
|
+
ast: ASTNode;
|
|
125
|
+
variable: {
|
|
126
|
+
name: string;
|
|
127
|
+
filters: string[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
constructor(templateText: string, ast: ASTNode, variableInfo: { name: string; filters: string[] });
|
|
131
|
+
|
|
132
|
+
static parse(templateText: string): VariableTemplate;
|
|
133
|
+
|
|
134
|
+
extractVariable(): {
|
|
135
|
+
name: string;
|
|
136
|
+
filters: string[];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
format(variableData: VariableValue | Variables): VariableValue;
|
|
140
|
+
}
|
|
141
|
+
|
|
122
142
|
export const ParserRules: any[];
|
|
123
143
|
export const VariableTypes: string[];
|
|
124
144
|
export const TemplateFilters: TemplateFiltersType;
|
package/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
const RuleTemplate = require('./src/
|
|
1
|
+
const RuleTemplate = require('./src/RuleTemplate');
|
|
2
2
|
const GeneralTemplate = require('./src/GeneralTemplate');
|
|
3
|
+
const VariableTemplate = require('./src/VariableTemplate');
|
|
3
4
|
|
|
4
5
|
module.exports.RuleTemplate = RuleTemplate;
|
|
5
6
|
module.exports.ParserRules = RuleTemplate.ParserRules;
|
|
6
7
|
module.exports.VariableTypes = RuleTemplate.VariableTypes;
|
|
7
8
|
module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
|
|
8
9
|
module.exports.GeneralTemplate = GeneralTemplate;
|
|
10
|
+
module.exports.VariableTemplate = VariableTemplate;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@halleyassist/rule-templater",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.14",
|
|
4
4
|
"description": "The grammar for HalleyAssist rules",
|
|
5
|
-
"main": "src/
|
|
5
|
+
"main": "src/RuleTemplate.production.js",
|
|
6
6
|
"browser": "./dist/rule-templater.browser.js",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
8
|
"scripts": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./RuleTemplate.production.js');
|
|
@@ -15,6 +15,7 @@ const VariableTypes = [
|
|
|
15
15
|
'time period',
|
|
16
16
|
'time period ago',
|
|
17
17
|
'time value',
|
|
18
|
+
'number time',
|
|
18
19
|
'string array',
|
|
19
20
|
'number array',
|
|
20
21
|
'boolean array',
|
|
@@ -28,6 +29,7 @@ const AllowedTypeMapping = {
|
|
|
28
29
|
'time period': ['time_period_atom'],
|
|
29
30
|
'time period ago': ['time_period_atom'],
|
|
30
31
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
32
|
+
'number time': ['number_atom'],
|
|
31
33
|
'string array': ['string_array'],
|
|
32
34
|
'number array': ['number_array'],
|
|
33
35
|
'boolean array': ['boolean_array'],
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// Note: We are coupled closely with the ebnf grammar structure of rule-parser
|
|
2
|
+
const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
|
|
3
|
+
TemplateFilters = require('./TemplateFilters'),
|
|
4
|
+
RuleParser = require('@halleyassist/rule-parser'),
|
|
5
|
+
RuleParserRules = RuleParser.ParserRules,
|
|
6
|
+
{Parser} = require('ebnf');
|
|
7
|
+
|
|
8
|
+
let ParserCache = null;
|
|
9
|
+
|
|
10
|
+
const VariableTypes = [
|
|
11
|
+
'string',
|
|
12
|
+
'number',
|
|
13
|
+
'boolean',
|
|
14
|
+
'object',
|
|
15
|
+
'time period',
|
|
16
|
+
'time period ago',
|
|
17
|
+
'time value',
|
|
18
|
+
'number time',
|
|
19
|
+
'string array',
|
|
20
|
+
'number array',
|
|
21
|
+
'boolean array',
|
|
22
|
+
'object array'
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const AllowedTypeMapping = {
|
|
26
|
+
'string': ['string_atom', 'string_concat'],
|
|
27
|
+
'number': ['number_atom', 'math_expr'],
|
|
28
|
+
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
29
|
+
'time period': ['time_period_atom'],
|
|
30
|
+
'time period ago': ['time_period_atom'],
|
|
31
|
+
'time value': ['time_value_atom', 'tod_atom'],
|
|
32
|
+
'number time': ['number_atom'],
|
|
33
|
+
'string array': ['string_array'],
|
|
34
|
+
'number array': ['number_array'],
|
|
35
|
+
'boolean array': ['boolean_array'],
|
|
36
|
+
'object': ['object_atom'],
|
|
37
|
+
'object array': ['object_array']
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Merge the base grammar with template-specific grammar rules
|
|
41
|
+
const extendedGrammar = [...RuleParserRules]
|
|
42
|
+
for(const rule of TemplateGrammar){
|
|
43
|
+
const idx = extendedGrammar.findIndex(r => r.name === rule.name);
|
|
44
|
+
if(idx !== -1){
|
|
45
|
+
extendedGrammar[idx] = rule;
|
|
46
|
+
} else {
|
|
47
|
+
extendedGrammar.push(rule);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
52
|
+
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
53
|
+
if (valueAtomIdx !== -1) {
|
|
54
|
+
extendedGrammar[valueAtomIdx] = Object.assign({}, extendedGrammar[valueAtomIdx]);
|
|
55
|
+
extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Export the parser rules for potential external use
|
|
59
|
+
const ParserRules = extendedGrammar;
|
|
60
|
+
|
|
61
|
+
class RuleTemplate {
|
|
62
|
+
constructor(ruleTemplateText, ast) {
|
|
63
|
+
this.ruleTemplateText = ruleTemplateText;
|
|
64
|
+
this.ast = ast;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse a rule template string and return a RuleTemplate instance
|
|
69
|
+
* @param {string} ruleTemplate - The template string to parse
|
|
70
|
+
* @returns {RuleTemplate} Instance with AST and template text
|
|
71
|
+
*/
|
|
72
|
+
static parse(ruleTemplate){
|
|
73
|
+
if(!ParserCache){
|
|
74
|
+
ParserCache = new Parser(ParserRules, {debug: false})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ast = RuleParser.toAst(ruleTemplate.trim(), ParserCache);
|
|
78
|
+
return new RuleTemplate(ruleTemplate, ast);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract variables from the template using the AST
|
|
83
|
+
* @returns {Array} Array of {name, filters: [], positions: [{start, end}]} objects
|
|
84
|
+
*/
|
|
85
|
+
extractVariables(){
|
|
86
|
+
const variables = [];
|
|
87
|
+
const variableMap = new Map();
|
|
88
|
+
|
|
89
|
+
const traverse = (node) => {
|
|
90
|
+
if (!node) return;
|
|
91
|
+
|
|
92
|
+
// Check if this is a template_value node
|
|
93
|
+
if (node.type === 'template_value') {
|
|
94
|
+
// Extract the variable information
|
|
95
|
+
const varInfo = this._extractVariableFromNode(node);
|
|
96
|
+
if (varInfo) {
|
|
97
|
+
// Add position to existing variable or create new entry
|
|
98
|
+
if (variableMap.has(varInfo.name)) {
|
|
99
|
+
const existing = variableMap.get(varInfo.name);
|
|
100
|
+
existing.positions.push({
|
|
101
|
+
start: varInfo.start,
|
|
102
|
+
end: varInfo.end
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
variableMap.set(varInfo.name, {
|
|
106
|
+
name: varInfo.name,
|
|
107
|
+
filters: varInfo.filters,
|
|
108
|
+
positions: [{
|
|
109
|
+
start: varInfo.start,
|
|
110
|
+
end: varInfo.end
|
|
111
|
+
}]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Traverse children
|
|
118
|
+
if (node.children) {
|
|
119
|
+
for (const child of node.children) {
|
|
120
|
+
traverse(child);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
traverse(this.ast);
|
|
126
|
+
|
|
127
|
+
// Convert map to array
|
|
128
|
+
for (const variable of variableMap.values()) {
|
|
129
|
+
variables.push(variable);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return variables;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extract function calls from the template using the AST
|
|
137
|
+
* @returns {Array} Array of unique function names used in the template
|
|
138
|
+
*/
|
|
139
|
+
extractFunctions(){
|
|
140
|
+
const functions = new Set();
|
|
141
|
+
|
|
142
|
+
const traverse = (node) => {
|
|
143
|
+
if (!node) return;
|
|
144
|
+
|
|
145
|
+
// Check if this is a function call node
|
|
146
|
+
if (node.type === 'fcall') {
|
|
147
|
+
// Find the function name in children
|
|
148
|
+
const fnameNode = node.children?.find(c => c.type === 'fname');
|
|
149
|
+
if (fnameNode && fnameNode.text) {
|
|
150
|
+
functions.add(fnameNode.text.trim());
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Traverse children
|
|
155
|
+
if (node.children) {
|
|
156
|
+
for (const child of node.children) {
|
|
157
|
+
traverse(child);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
traverse(this.ast);
|
|
163
|
+
|
|
164
|
+
// Convert set to sorted array for consistent output
|
|
165
|
+
return Array.from(functions).sort();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Extract variable name and filters from a template_value AST node
|
|
170
|
+
* @private
|
|
171
|
+
*/
|
|
172
|
+
_extractVariableFromNode(node) {
|
|
173
|
+
if (node.type !== 'template_value') return null;
|
|
174
|
+
|
|
175
|
+
// Find the template_expr child
|
|
176
|
+
const templateExpr = node.children?.find(c => c.type === 'template_expr');
|
|
177
|
+
if (!templateExpr) return null;
|
|
178
|
+
|
|
179
|
+
// Extract the path (variable name)
|
|
180
|
+
const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
|
|
181
|
+
if (!templatePath || !templatePath.text) return null;
|
|
182
|
+
|
|
183
|
+
const name = templatePath.text.trim();
|
|
184
|
+
|
|
185
|
+
// Extract filters
|
|
186
|
+
const filters = [];
|
|
187
|
+
for (const child of templateExpr.children || []) {
|
|
188
|
+
if (child.type === 'template_filter_call') {
|
|
189
|
+
const filterName = this._extractFilterName(child);
|
|
190
|
+
if (filterName) {
|
|
191
|
+
filters.push(filterName);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Extract position information from the node
|
|
197
|
+
const start = node.start;
|
|
198
|
+
const end = node.end;
|
|
199
|
+
|
|
200
|
+
return { name, filters, start, end };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extract filter name from template_filter_call node
|
|
205
|
+
* @private
|
|
206
|
+
*/
|
|
207
|
+
_extractFilterName(node) {
|
|
208
|
+
const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
|
|
209
|
+
if (!filterNameNode || !filterNameNode.text) return null;
|
|
210
|
+
|
|
211
|
+
return filterNameNode.text.trim();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Validate variable types against the AST
|
|
216
|
+
* @param {Object} variables - Object mapping variable names to {type} objects
|
|
217
|
+
* @returns {Object} Object with validation results: {valid: boolean, errors: []}
|
|
218
|
+
*/
|
|
219
|
+
validate(variables) {
|
|
220
|
+
if (!variables || typeof variables !== 'object') {
|
|
221
|
+
return {
|
|
222
|
+
valid: false,
|
|
223
|
+
errors: ['Variables must be provided as an object']
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const errors = [];
|
|
228
|
+
const extractedVars = this.extractVariables();
|
|
229
|
+
|
|
230
|
+
for (const varInfo of extractedVars) {
|
|
231
|
+
const varName = varInfo.name;
|
|
232
|
+
|
|
233
|
+
// Check if variable is provided
|
|
234
|
+
if (!variables.hasOwnProperty(varName)) {
|
|
235
|
+
errors.push(`Variable '${varName}' not provided in variables object`);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const varData = variables[varName];
|
|
240
|
+
if (typeof varData !== 'object') {
|
|
241
|
+
errors.push(`Variable '${varName}' must be an object`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { type } = varData;
|
|
246
|
+
|
|
247
|
+
// Validate type if provided
|
|
248
|
+
if (type && !VariableTypes.includes(type)) {
|
|
249
|
+
errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
valid: errors.length === 0,
|
|
255
|
+
errors
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Prepare the template by replacing variables with their values
|
|
261
|
+
* Rebuilds from AST by iterating through children
|
|
262
|
+
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
263
|
+
* @returns {string} The prepared rule string
|
|
264
|
+
*/
|
|
265
|
+
prepare(variables){
|
|
266
|
+
if (!variables || typeof variables !== 'object') {
|
|
267
|
+
throw new Error('Variables must be provided as an object');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Rebuild the rule string from AST
|
|
271
|
+
return this._rebuildFromAST(this.ast, variables);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Rebuild rule string from AST node, replacing template_value nodes with variable values
|
|
276
|
+
* @private
|
|
277
|
+
* @param {Object} node - AST node
|
|
278
|
+
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
279
|
+
* @returns {string} Rebuilt string
|
|
280
|
+
*/
|
|
281
|
+
_rebuildFromAST(node, variables) {
|
|
282
|
+
if (!node) return '';
|
|
283
|
+
|
|
284
|
+
// If this is a template_value node, replace it with the computed value
|
|
285
|
+
if (node.type === 'template_value') {
|
|
286
|
+
return this._computeTemplateReplacement(node, variables);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// If node has no children, it's a leaf - return its text
|
|
290
|
+
if (!node.children || node.children.length === 0) {
|
|
291
|
+
return node.text || '';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Node has children - rebuild by iterating through children and preserving gaps
|
|
295
|
+
let result = '';
|
|
296
|
+
const originalText = node.text || '';
|
|
297
|
+
let lastEnd = node.start || 0;
|
|
298
|
+
|
|
299
|
+
for (const child of node.children) {
|
|
300
|
+
// Add any text between the last child's end and this child's start (gaps/syntax)
|
|
301
|
+
if (child.start !== undefined && child.start > lastEnd) {
|
|
302
|
+
result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Add the child's rebuilt text
|
|
306
|
+
result += this._rebuildFromAST(child, variables);
|
|
307
|
+
|
|
308
|
+
// Update lastEnd to this child's end position
|
|
309
|
+
if (child.end !== undefined) {
|
|
310
|
+
lastEnd = child.end;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Add any remaining text after the last child
|
|
315
|
+
if (node.end !== undefined && lastEnd < node.end) {
|
|
316
|
+
result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Compute the replacement value for a template_value node
|
|
324
|
+
* @private
|
|
325
|
+
* @param {Object} node - template_value AST node
|
|
326
|
+
* @param {Object} variables - Object mapping variable names to {value, type} objects
|
|
327
|
+
* @returns {string} Replacement string
|
|
328
|
+
*/
|
|
329
|
+
_computeTemplateReplacement(node, variables) {
|
|
330
|
+
const templateInfo = this._extractVariableFromNode(node);
|
|
331
|
+
if (!templateInfo) {
|
|
332
|
+
throw new Error(`Failed to extract variable information from template node`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const varName = templateInfo.name;
|
|
336
|
+
|
|
337
|
+
// Validate variable is provided
|
|
338
|
+
if (!variables.hasOwnProperty(varName)) {
|
|
339
|
+
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let varData = variables[varName];
|
|
343
|
+
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
344
|
+
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
varData = Object.assign({}, varData);
|
|
348
|
+
|
|
349
|
+
// Require type property for all variables
|
|
350
|
+
if (!varData.hasOwnProperty('type')) {
|
|
351
|
+
throw new Error(`Variable '${varName}' must have a 'type' property`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Validate type
|
|
355
|
+
if (!VariableTypes.includes(varData.type)) {
|
|
356
|
+
throw new Error(`Invalid variable type '${varData.type}' for variable '${varName}'`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Apply filters if present
|
|
360
|
+
if (templateInfo.filters && templateInfo.filters.length > 0) {
|
|
361
|
+
for (const filterName of templateInfo.filters) {
|
|
362
|
+
if (!TemplateFilters[filterName]) {
|
|
363
|
+
throw new Error(`Unknown filter '${filterName}'`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
TemplateFilters[filterName](varData);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return this._serializeVarData(varData, varName);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
_serializeVarData(varData, varName) {
|
|
374
|
+
const { value, type } = varData;
|
|
375
|
+
|
|
376
|
+
if (!VariableTypes.includes(type)) {
|
|
377
|
+
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (type === 'string') {
|
|
381
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (type === 'number') {
|
|
385
|
+
return String(value);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (type === 'boolean') {
|
|
389
|
+
return value ? 'true' : 'false';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (type === 'time period' || type === 'time period ago') {
|
|
393
|
+
let ret = `${value.from} TO ${value.to}`;
|
|
394
|
+
if(value.ago) {
|
|
395
|
+
ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
|
|
396
|
+
}
|
|
397
|
+
return ret;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return String(value);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Helper method to validate if an AST node matches a variable type
|
|
405
|
+
* @param {Object} astNode - The AST node to validate
|
|
406
|
+
* @param {string} variableType - The expected variable type
|
|
407
|
+
* @returns {boolean} True if valid, false otherwise
|
|
408
|
+
*/
|
|
409
|
+
static validateVariableNode(astNode, variableType) {
|
|
410
|
+
if (!astNode || !astNode.type) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const allowedTypes = AllowedTypeMapping[variableType];
|
|
415
|
+
if (!allowedTypes) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return allowedTypes.includes(astNode.type);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
RuleTemplate.ParserRules = ParserRules;
|
|
424
|
+
RuleTemplate.VariableTypes = VariableTypes;
|
|
425
|
+
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
426
|
+
|
|
427
|
+
module.exports = RuleTemplate;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = require('./
|
|
1
|
+
module.exports = require('./RuleTemplate.production.js');
|
|
@@ -13,7 +13,6 @@ const VariableTypes = [
|
|
|
13
13
|
'boolean',
|
|
14
14
|
'object',
|
|
15
15
|
'time period',
|
|
16
|
-
'time period ago',
|
|
17
16
|
'time value',
|
|
18
17
|
'string array',
|
|
19
18
|
'number array',
|
|
@@ -26,7 +25,6 @@ const AllowedTypeMapping = {
|
|
|
26
25
|
'number': ['number_atom', 'math_expr'],
|
|
27
26
|
'boolean': ['boolean_atom', 'boolean_expr'],
|
|
28
27
|
'time period': ['time_period_atom'],
|
|
29
|
-
'time period ago': ['time_period_atom'],
|
|
30
28
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
31
29
|
'string array': ['string_array'],
|
|
32
30
|
'number array': ['number_array'],
|
|
@@ -49,8 +47,7 @@ for(const rule of TemplateGrammar){
|
|
|
49
47
|
// Add template_value as an alternative to value_atom so templates can be parsed
|
|
50
48
|
const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
|
|
51
49
|
if (valueAtomIdx !== -1) {
|
|
52
|
-
extendedGrammar[valueAtomIdx]
|
|
53
|
-
extendedGrammar[valueAtomIdx].bnf = extendedGrammar[valueAtomIdx].bnf.concat([['template_value']]);
|
|
50
|
+
extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
// Export the parser rules for potential external use
|
|
@@ -337,12 +334,12 @@ class RuleTemplate {
|
|
|
337
334
|
throw new Error(`Variable '${varName}' not provided in variables object`);
|
|
338
335
|
}
|
|
339
336
|
|
|
340
|
-
|
|
337
|
+
const varData = variables[varName];
|
|
341
338
|
if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
|
|
342
339
|
throw new Error(`Variable '${varName}' must be an object with 'value' property`);
|
|
343
340
|
}
|
|
344
|
-
|
|
345
|
-
|
|
341
|
+
|
|
342
|
+
let { value, type } = varData;
|
|
346
343
|
|
|
347
344
|
// Require type property for all variables
|
|
348
345
|
if (!varData.hasOwnProperty('type')) {
|
|
@@ -350,8 +347,8 @@ class RuleTemplate {
|
|
|
350
347
|
}
|
|
351
348
|
|
|
352
349
|
// Validate type
|
|
353
|
-
if (!VariableTypes.includes(
|
|
354
|
-
throw new Error(`Invalid variable type '${
|
|
350
|
+
if (!VariableTypes.includes(type)) {
|
|
351
|
+
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
355
352
|
}
|
|
356
353
|
|
|
357
354
|
// Apply filters if present
|
|
@@ -360,42 +357,27 @@ class RuleTemplate {
|
|
|
360
357
|
if (!TemplateFilters[filterName]) {
|
|
361
358
|
throw new Error(`Unknown filter '${filterName}'`);
|
|
362
359
|
}
|
|
363
|
-
|
|
364
|
-
TemplateFilters[filterName](varData);
|
|
360
|
+
value = TemplateFilters[filterName](value);
|
|
365
361
|
}
|
|
362
|
+
// After applying filters, the result is already a string representation
|
|
363
|
+
return String(value);
|
|
366
364
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
_serializeVarData(varData, varName) {
|
|
372
|
-
const { value, type } = varData;
|
|
373
|
-
|
|
374
|
-
if (!VariableTypes.includes(type)) {
|
|
375
|
-
throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
376
|
-
}
|
|
377
|
-
|
|
365
|
+
|
|
366
|
+
// Convert value to string representation based on type
|
|
378
367
|
if (type === 'string') {
|
|
368
|
+
// Escape backslashes first, then quotes in string values.
|
|
369
|
+
// Order is critical: escaping backslashes first prevents double-escaping.
|
|
370
|
+
// E.g., "test\" becomes "test\\" then "test\\\"" (correct)
|
|
371
|
+
// If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
|
|
379
372
|
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (type === 'number') {
|
|
373
|
+
} else if (type === 'number') {
|
|
383
374
|
return String(value);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (type === 'boolean') {
|
|
375
|
+
} else if (type === 'boolean') {
|
|
387
376
|
return value ? 'true' : 'false';
|
|
377
|
+
} else {
|
|
378
|
+
// Default behavior - just insert the value as-is
|
|
379
|
+
return String(value);
|
|
388
380
|
}
|
|
389
|
-
|
|
390
|
-
if (type === 'time period' || type === 'time period ago') {
|
|
391
|
-
let ret = `${value.from} TO ${value.to}`;
|
|
392
|
-
if(value.ago) {
|
|
393
|
-
ret += ` AGO ${value.ago[0]} ${value.ago[1]}`;
|
|
394
|
-
}
|
|
395
|
-
return ret;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return String(value);
|
|
399
381
|
}
|
|
400
382
|
|
|
401
383
|
/**
|
|
@@ -418,8 +400,8 @@ class RuleTemplate {
|
|
|
418
400
|
}
|
|
419
401
|
}
|
|
420
402
|
|
|
421
|
-
|
|
422
|
-
RuleTemplate.VariableTypes = VariableTypes;
|
|
423
|
-
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
424
|
-
|
|
403
|
+
// Export the class and parser rules
|
|
425
404
|
module.exports = RuleTemplate;
|
|
405
|
+
module.exports.ParserRules = ParserRules;
|
|
406
|
+
module.exports.VariableTypes = VariableTypes;
|
|
407
|
+
module.exports.TemplateFilters = TemplateFilters;
|
package/src/TemplateFilters.js
CHANGED
|
@@ -52,7 +52,9 @@ const TemplateFilters = {
|
|
|
52
52
|
number: varData => {
|
|
53
53
|
varData.value = Number(varData.value);
|
|
54
54
|
varData.type = 'number';
|
|
55
|
-
|
|
55
|
+
if(isNaN(varData.value)){
|
|
56
|
+
throw new Error(`Value "${varData.value}" cannot be converted to a number`);
|
|
57
|
+
}
|
|
56
58
|
},
|
|
57
59
|
|
|
58
60
|
// Convert to boolean
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { Parser } = require('ebnf');
|
|
2
|
+
const RuleTemplate = require('./RuleTemplate');
|
|
3
|
+
const TemplateFilters = require('./TemplateFilters');
|
|
4
|
+
|
|
5
|
+
let ParserCache = null;
|
|
6
|
+
|
|
7
|
+
class VariableTemplate {
|
|
8
|
+
constructor(templateText, ast, variableInfo) {
|
|
9
|
+
this.templateText = templateText;
|
|
10
|
+
this.ast = ast;
|
|
11
|
+
this.variable = variableInfo;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static parse(templateText) {
|
|
15
|
+
if (typeof templateText !== 'string') {
|
|
16
|
+
throw new Error('Variable template must be a string');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const expressionText = VariableTemplate._normalizeExpression(templateText);
|
|
20
|
+
if (!expressionText) {
|
|
21
|
+
throw new Error('Variable template cannot be empty');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!ParserCache) {
|
|
25
|
+
ParserCache = new Parser(RuleTemplate.ParserRules, { debug: false });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ast = ParserCache.getAST(expressionText, 'template_expr');
|
|
29
|
+
const variableInfo = VariableTemplate._extractVariableFromAst(ast);
|
|
30
|
+
if (!variableInfo) {
|
|
31
|
+
throw new Error('Invalid variable template expression');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return new VariableTemplate(templateText, ast, variableInfo);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
extractVariable() {
|
|
38
|
+
return {
|
|
39
|
+
name: this.variable.name,
|
|
40
|
+
filters: this.variable.filters.slice()
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
format(variableData) {
|
|
45
|
+
let varData = variableData;
|
|
46
|
+
|
|
47
|
+
if (!varData || typeof varData !== 'object') {
|
|
48
|
+
throw new Error('Variable data must be provided as an object');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!Object.prototype.hasOwnProperty.call(varData, 'value')) {
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(varData, this.variable.name)) {
|
|
53
|
+
throw new Error(`Variable '${this.variable.name}' not provided`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
varData = varData[this.variable.name];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!varData || typeof varData !== 'object' || !Object.prototype.hasOwnProperty.call(varData, 'value')) {
|
|
60
|
+
throw new Error(`Variable '${this.variable.name}' must be an object with 'value' property`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
varData = VariableTemplate._cloneVarData(varData);
|
|
64
|
+
|
|
65
|
+
for (const filterName of this.variable.filters) {
|
|
66
|
+
if (!TemplateFilters[filterName]) {
|
|
67
|
+
throw new Error(`Unknown filter '${filterName}'`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
TemplateFilters[filterName](varData);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return varData;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static _normalizeExpression(templateText) {
|
|
77
|
+
const trimmed = templateText.trim();
|
|
78
|
+
if (trimmed.startsWith('${') && trimmed.endsWith('}')) {
|
|
79
|
+
return trimmed.slice(2, -1).trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
static _extractVariableFromAst(ast) {
|
|
86
|
+
if (!ast || ast.type !== 'template_expr') {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const templatePath = ast.children?.find(c => c.type === 'template_path');
|
|
91
|
+
if (!templatePath || !templatePath.text) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const filters = [];
|
|
96
|
+
for (const child of ast.children || []) {
|
|
97
|
+
if (child.type === 'template_filter_call') {
|
|
98
|
+
const filterNameNode = child.children?.find(c => c.type === 'template_filter_name');
|
|
99
|
+
const filterName = filterNameNode?.text?.trim();
|
|
100
|
+
if (filterName) {
|
|
101
|
+
filters.push(filterName);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
name: templatePath.text.trim(),
|
|
108
|
+
filters
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static _cloneVarData(varData) {
|
|
113
|
+
const cloned = Object.assign({}, varData);
|
|
114
|
+
if (cloned.value && typeof cloned.value === 'object') {
|
|
115
|
+
if (Array.isArray(cloned.value)) {
|
|
116
|
+
cloned.value = cloned.value.slice();
|
|
117
|
+
} else {
|
|
118
|
+
cloned.value = Object.assign({}, cloned.value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return cloned;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = VariableTemplate;
|