@halleyassist/rule-templater 0.0.13 → 0.0.15
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 +428 -6
- package/index.d.ts +15 -2
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/RuleTemplate.ebnf.js +17 -0
- package/src/RuleTemplate.js +21 -0
- package/src/RuleTemplate.production.ebnf.js +1 -1
- package/src/RuleTemplate.production.js +21 -0
- package/src/TemplateFilters.js +3 -1
- package/src/VariableValidate.js +345 -0
package/README.md
CHANGED
|
@@ -305,6 +305,25 @@ const result = parsed.prepare({ EVENT: { value: 'test' } });
|
|
|
305
305
|
// Result: EventIs(tset)
|
|
306
306
|
```
|
|
307
307
|
|
|
308
|
+
### `VariableValidate`
|
|
309
|
+
|
|
310
|
+
BNF-backed validators for each supported variable type.
|
|
311
|
+
|
|
312
|
+
**Example:**
|
|
313
|
+
```javascript
|
|
314
|
+
const { VariableValidate } = require('@halleyassist/rule-templater');
|
|
315
|
+
|
|
316
|
+
const result = VariableValidate.validate({
|
|
317
|
+
value: { from: '08:00', to: '12:00', ago: [2, 'HOURS'] },
|
|
318
|
+
type: 'time period ago'
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
console.log(result.valid);
|
|
322
|
+
// true
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Use `VariableValidate.validateValue(type, value)` to validate a raw value against a specific variable type.
|
|
326
|
+
|
|
308
327
|
## Variable Types
|
|
309
328
|
|
|
310
329
|
The following variable types are supported:
|
|
@@ -316,11 +335,81 @@ The following variable types are supported:
|
|
|
316
335
|
- `time period`
|
|
317
336
|
- `time period ago`
|
|
318
337
|
- `time value`
|
|
338
|
+
- `number time`
|
|
319
339
|
- `string array`
|
|
320
340
|
- `number array`
|
|
321
341
|
- `boolean array`
|
|
322
342
|
- `object array`
|
|
323
343
|
|
|
344
|
+
### Time Type Formats
|
|
345
|
+
|
|
346
|
+
The time-related variable types use the following structures when passed to `prepare()` or `validate()`:
|
|
347
|
+
|
|
348
|
+
#### `time value`
|
|
349
|
+
|
|
350
|
+
Use a time-of-day string such as `08:00`.
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
{
|
|
354
|
+
value: '08:00',
|
|
355
|
+
type: 'time value'
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
This is useful when a rule expects a single time value, for example:
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
const prepared = parsed.prepare({
|
|
363
|
+
START_TIME: { value: '08:00', type: 'time value' }
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### `time period`
|
|
368
|
+
|
|
369
|
+
Use an object with `from` and `to` properties, both using the same time-of-day string format as `time value`.
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
{
|
|
373
|
+
value: {
|
|
374
|
+
from: '08:00',
|
|
375
|
+
to: '12:00'
|
|
376
|
+
},
|
|
377
|
+
type: 'time period'
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
When rendered into a rule template, this becomes:
|
|
382
|
+
|
|
383
|
+
```text
|
|
384
|
+
08:00 TO 12:00
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### `time period ago`
|
|
388
|
+
|
|
389
|
+
Use the same structure as `time period`, plus an `ago` tuple containing:
|
|
390
|
+
|
|
391
|
+
- the numeric offset
|
|
392
|
+
- the rule time unit token, for example `HOURS`
|
|
393
|
+
|
|
394
|
+
```javascript
|
|
395
|
+
{
|
|
396
|
+
value: {
|
|
397
|
+
from: '08:00',
|
|
398
|
+
to: '12:00',
|
|
399
|
+
ago: [2, 'HOURS']
|
|
400
|
+
},
|
|
401
|
+
type: 'time period ago'
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
When rendered into a rule template, this becomes:
|
|
406
|
+
|
|
407
|
+
```text
|
|
408
|
+
08:00 TO 12:00 AGO 2 HOURS
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
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`.
|
|
412
|
+
|
|
324
413
|
## License
|
|
325
414
|
|
|
326
415
|
ISC
|
|
@@ -3645,12 +3645,66 @@ exports.Grammars = require("./Grammars");
|
|
|
3645
3645
|
|
|
3646
3646
|
},{"./Grammars":8,"./Parser":9,"./ParsingError":10,"./TokenError":12}],14:[function(require,module,exports){
|
|
3647
3647
|
module.exports = require('./RuleTemplate.production.js');
|
|
3648
|
-
},{"./RuleTemplate.production.js":
|
|
3649
|
-
|
|
3650
|
-
|
|
3648
|
+
},{"./RuleTemplate.production.js":17}],15:[function(require,module,exports){
|
|
3649
|
+
const {Grammars} = require('ebnf');
|
|
3650
|
+
|
|
3651
|
+
const grammar = `
|
|
3652
|
+
TEMPLATE_BEGIN ::= "\${"
|
|
3653
|
+
TEMPLATE_END ::= "}"
|
|
3654
|
+
PIPE ::= "|"
|
|
3655
|
+
IDENT ::= [A-Za-z_][A-Za-z0-9_]*
|
|
3656
|
+
DOT ::= "."
|
|
3657
|
+
|
|
3658
|
+
template_value ::= TEMPLATE_BEGIN WS* template_expr WS* TEMPLATE_END
|
|
3659
|
+
|
|
3660
|
+
template_expr ::= template_path (WS* template_pipe WS* template_filter_call)*
|
|
3661
|
+
|
|
3662
|
+
template_pipe ::= PIPE
|
|
3663
|
+
|
|
3664
|
+
template_path ::= IDENT (WS* DOT WS* IDENT)*
|
|
3665
|
+
|
|
3666
|
+
template_filter_call ::= template_filter_name (WS* BEGIN_ARGUMENT WS* template_filter_args? WS* END_ARGUMENT)?
|
|
3667
|
+
|
|
3668
|
+
template_filter_name ::= IDENT
|
|
3669
|
+
|
|
3670
|
+
template_filter_args ::= template_filter_arg (WS* "," WS* template_filter_arg)*
|
|
3671
|
+
|
|
3672
|
+
template_filter_arg ::= value | template_value
|
|
3673
|
+
|
|
3674
|
+
number_atom ::= number | template_value
|
|
3675
|
+
number_time_atom ::= number_time | template_value WS+ unit | template_value
|
|
3676
|
+
tod_atom ::= number_tod | template_value
|
|
3677
|
+
dow_atom ::= dow | template_value
|
|
3678
|
+
between_time_only_atom ::= between_time_only | template_value
|
|
3679
|
+
between_tod_only_atom ::= between_tod_only | template_value
|
|
3680
|
+
|
|
3681
|
+
string_atom ::= string
|
|
3682
|
+
boolean_atom ::= false | true
|
|
3683
|
+
time_value_atom ::= number_tod
|
|
3684
|
+
time_period_atom ::= time_value_atom WS* "TO" WS* time_value_atom
|
|
3685
|
+
time_period_ago_atom ::= time_value_atom WS* "TO" WS* time_value_atom WS+ AGO WS+ number WS+ unit
|
|
3686
|
+
|
|
3687
|
+
object_atom ::= json_object
|
|
3688
|
+
json_value ::= string | number | false | true | null | json_array | json_object
|
|
3689
|
+
json_member ::= string NAME_SEPARATOR json_value
|
|
3690
|
+
json_object ::= BEGIN_OBJECT (json_member (VALUE_SEPARATOR json_member)*)? END_OBJECT
|
|
3691
|
+
json_array ::= BEGIN_ARRAY (json_value (VALUE_SEPARATOR json_value)*)? END_ARRAY
|
|
3692
|
+
|
|
3693
|
+
string_array ::= BEGIN_ARRAY (string (VALUE_SEPARATOR string)*)? END_ARRAY
|
|
3694
|
+
number_array ::= BEGIN_ARRAY (number (VALUE_SEPARATOR number)*)? END_ARRAY
|
|
3695
|
+
boolean_array ::= BEGIN_ARRAY (boolean_atom (VALUE_SEPARATOR boolean_atom)*)? END_ARRAY
|
|
3696
|
+
object_array ::= BEGIN_ARRAY (json_object (VALUE_SEPARATOR json_object)*)? END_ARRAY
|
|
3697
|
+
`
|
|
3698
|
+
|
|
3699
|
+
module.exports = Grammars.W3C.getRules(grammar);
|
|
3700
|
+
|
|
3701
|
+
},{"ebnf":13}],16:[function(require,module,exports){
|
|
3702
|
+
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"]]},{"name":"string_atom","bnf":[["string"]]},{"name":"boolean_atom","bnf":[["false"],["true"]]},{"name":"time_value_atom","bnf":[["number_tod"]]},{"name":"time_period_atom","bnf":[["time_value_atom","WS*","\"TO\"","WS*","time_value_atom"]]},{"name":"time_period_ago_atom","bnf":[["time_value_atom","WS*","\"TO\"","WS*","time_value_atom","WS+","AGO","WS+","number","WS+","unit"]]},{"name":"object_atom","bnf":[["json_object"]]},{"name":"json_value","bnf":[["string"],["number"],["false"],["true"],["null"],["json_array"],["json_object"]]},{"name":"json_member","bnf":[["string","NAME_SEPARATOR","json_value"]]},{"name":"%json_object[2][2]","bnf":[["VALUE_SEPARATOR","json_member"]],"fragment":true},{"name":"%json_object[2]","bnf":[["json_member","%json_object[2][2]*"]],"fragment":true},{"name":"json_object","bnf":[["BEGIN_OBJECT","%json_object[2]?","END_OBJECT"]]},{"name":"%json_array[2][2]","bnf":[["VALUE_SEPARATOR","json_value"]],"fragment":true},{"name":"%json_array[2]","bnf":[["json_value","%json_array[2][2]*"]],"fragment":true},{"name":"json_array","bnf":[["BEGIN_ARRAY","%json_array[2]?","END_ARRAY"]]},{"name":"%string_array[2][2]","bnf":[["VALUE_SEPARATOR","string"]],"fragment":true},{"name":"%string_array[2]","bnf":[["string","%string_array[2][2]*"]],"fragment":true},{"name":"string_array","bnf":[["BEGIN_ARRAY","%string_array[2]?","END_ARRAY"]]},{"name":"%number_array[2][2]","bnf":[["VALUE_SEPARATOR","number"]],"fragment":true},{"name":"%number_array[2]","bnf":[["number","%number_array[2][2]*"]],"fragment":true},{"name":"number_array","bnf":[["BEGIN_ARRAY","%number_array[2]?","END_ARRAY"]]},{"name":"%boolean_array[2][2]","bnf":[["VALUE_SEPARATOR","boolean_atom"]],"fragment":true},{"name":"%boolean_array[2]","bnf":[["boolean_atom","%boolean_array[2][2]*"]],"fragment":true},{"name":"boolean_array","bnf":[["BEGIN_ARRAY","%boolean_array[2]?","END_ARRAY"]]},{"name":"%object_array[2][2]","bnf":[["VALUE_SEPARATOR","json_object"]],"fragment":true},{"name":"%object_array[2]","bnf":[["json_object","%object_array[2][2]*"]],"fragment":true},{"name":"object_array","bnf":[["BEGIN_ARRAY","%object_array[2]?","END_ARRAY"]]}]
|
|
3703
|
+
},{}],17:[function(require,module,exports){
|
|
3651
3704
|
// Note: We are coupled closely with the ebnf grammar structure of rule-parser
|
|
3652
3705
|
const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
|
|
3653
3706
|
TemplateFilters = require('./TemplateFilters'),
|
|
3707
|
+
VariableValidate = require('./VariableValidate'),
|
|
3654
3708
|
RuleParser = require('@halleyassist/rule-parser'),
|
|
3655
3709
|
RuleParserRules = RuleParser.ParserRules,
|
|
3656
3710
|
{Parser} = require('ebnf');
|
|
@@ -3665,6 +3719,7 @@ const VariableTypes = [
|
|
|
3665
3719
|
'time period',
|
|
3666
3720
|
'time period ago',
|
|
3667
3721
|
'time value',
|
|
3722
|
+
'number time',
|
|
3668
3723
|
'string array',
|
|
3669
3724
|
'number array',
|
|
3670
3725
|
'boolean array',
|
|
@@ -3678,6 +3733,7 @@ const AllowedTypeMapping = {
|
|
|
3678
3733
|
'time period': ['time_period_atom'],
|
|
3679
3734
|
'time period ago': ['time_period_atom'],
|
|
3680
3735
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
3736
|
+
'number time': ['number_atom'],
|
|
3681
3737
|
'string array': ['string_array'],
|
|
3682
3738
|
'number array': ['number_array'],
|
|
3683
3739
|
'boolean array': ['boolean_array'],
|
|
@@ -3895,6 +3951,14 @@ class RuleTemplate {
|
|
|
3895
3951
|
// Validate type if provided
|
|
3896
3952
|
if (type && !VariableTypes.includes(type)) {
|
|
3897
3953
|
errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
3954
|
+
continue;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
if (type) {
|
|
3958
|
+
const validation = VariableValidate.validate(varData);
|
|
3959
|
+
if (!validation.valid) {
|
|
3960
|
+
errors.push(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
3961
|
+
}
|
|
3898
3962
|
}
|
|
3899
3963
|
}
|
|
3900
3964
|
|
|
@@ -4015,6 +4079,11 @@ class RuleTemplate {
|
|
|
4015
4079
|
}
|
|
4016
4080
|
}
|
|
4017
4081
|
|
|
4082
|
+
const validation = VariableValidate.validate(varData);
|
|
4083
|
+
if (!validation.valid) {
|
|
4084
|
+
throw new Error(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4018
4087
|
return this._serializeVarData(varData, varName);
|
|
4019
4088
|
}
|
|
4020
4089
|
|
|
@@ -4045,6 +4114,10 @@ class RuleTemplate {
|
|
|
4045
4114
|
return ret;
|
|
4046
4115
|
}
|
|
4047
4116
|
|
|
4117
|
+
if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
|
|
4118
|
+
return JSON.stringify(value);
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4048
4121
|
return String(value);
|
|
4049
4122
|
}
|
|
4050
4123
|
|
|
@@ -4071,10 +4144,11 @@ class RuleTemplate {
|
|
|
4071
4144
|
RuleTemplate.ParserRules = ParserRules;
|
|
4072
4145
|
RuleTemplate.VariableTypes = VariableTypes;
|
|
4073
4146
|
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
4147
|
+
RuleTemplate.VariableValidate = VariableValidate;
|
|
4074
4148
|
|
|
4075
4149
|
module.exports = RuleTemplate;
|
|
4076
4150
|
|
|
4077
|
-
},{"./RuleTemplate.production.ebnf.js":
|
|
4151
|
+
},{"./RuleTemplate.production.ebnf.js":16,"./TemplateFilters":18,"./VariableValidate":19,"@halleyassist/rule-parser":2,"ebnf":13}],18:[function(require,module,exports){
|
|
4078
4152
|
/*
|
|
4079
4153
|
Template filters are functions that transform variable values.
|
|
4080
4154
|
They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
|
|
@@ -4129,7 +4203,9 @@ const TemplateFilters = {
|
|
|
4129
4203
|
number: varData => {
|
|
4130
4204
|
varData.value = Number(varData.value);
|
|
4131
4205
|
varData.type = 'number';
|
|
4132
|
-
|
|
4206
|
+
if(isNaN(varData.value)){
|
|
4207
|
+
throw new Error(`Value "${varData.value}" cannot be converted to a number`);
|
|
4208
|
+
}
|
|
4133
4209
|
},
|
|
4134
4210
|
|
|
4135
4211
|
// Convert to boolean
|
|
@@ -4232,5 +4308,351 @@ const TemplateFilters = {
|
|
|
4232
4308
|
}
|
|
4233
4309
|
|
|
4234
4310
|
module.exports = TemplateFilters;
|
|
4235
|
-
},{}]
|
|
4311
|
+
},{}],19:[function(require,module,exports){
|
|
4312
|
+
const { Parser } = require('ebnf');
|
|
4313
|
+
const TemplateGrammar = require('./RuleTemplate.ebnf');
|
|
4314
|
+
const RuleParser = require('@halleyassist/rule-parser');
|
|
4315
|
+
const RuleParserRules = RuleParser.ParserRules;
|
|
4316
|
+
|
|
4317
|
+
const VariableTypes = [
|
|
4318
|
+
'string',
|
|
4319
|
+
'number',
|
|
4320
|
+
'boolean',
|
|
4321
|
+
'object',
|
|
4322
|
+
'time period',
|
|
4323
|
+
'time period ago',
|
|
4324
|
+
'time value',
|
|
4325
|
+
'number time',
|
|
4326
|
+
'string array',
|
|
4327
|
+
'number array',
|
|
4328
|
+
'boolean array',
|
|
4329
|
+
'object array'
|
|
4330
|
+
];
|
|
4331
|
+
|
|
4332
|
+
let ParserCache = null;
|
|
4333
|
+
|
|
4334
|
+
const ValidationRules = [...RuleParserRules];
|
|
4335
|
+
for (const rule of TemplateGrammar) {
|
|
4336
|
+
const idx = ValidationRules.findIndex(existingRule => existingRule.name === rule.name);
|
|
4337
|
+
if (idx !== -1) {
|
|
4338
|
+
ValidationRules[idx] = rule;
|
|
4339
|
+
} else {
|
|
4340
|
+
ValidationRules.push(rule);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
|
|
4344
|
+
class VariableValidate {
|
|
4345
|
+
static validate(variableData) {
|
|
4346
|
+
if (!variableData || typeof variableData !== 'object' || Array.isArray(variableData)) {
|
|
4347
|
+
return {
|
|
4348
|
+
valid: false,
|
|
4349
|
+
error: 'Variable data must be an object with value and type properties'
|
|
4350
|
+
};
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
if (!Object.prototype.hasOwnProperty.call(variableData, 'type')) {
|
|
4354
|
+
return {
|
|
4355
|
+
valid: false,
|
|
4356
|
+
error: 'Variable data must include a type property'
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
return VariableValidate.validateValue(variableData.type, variableData.value);
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
static validateValue(type, value) {
|
|
4364
|
+
const validator = VariableValidate.validators[type];
|
|
4365
|
+
if (!validator) {
|
|
4366
|
+
return {
|
|
4367
|
+
valid: false,
|
|
4368
|
+
error: `Unsupported variable type '${type}'`
|
|
4369
|
+
};
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
try {
|
|
4373
|
+
return validator(value);
|
|
4374
|
+
} catch (error) {
|
|
4375
|
+
return {
|
|
4376
|
+
valid: false,
|
|
4377
|
+
error: error.message
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
static isValid(type, value) {
|
|
4383
|
+
return VariableValidate.validateValue(type, value).valid;
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
static _validateWithRule(value, startRule, options = {}) {
|
|
4387
|
+
const parser = VariableValidate._getParser();
|
|
4388
|
+
const normalized = options.normalize ? options.normalize(value) : value;
|
|
4389
|
+
|
|
4390
|
+
if (typeof normalized !== 'string' || !normalized.length) {
|
|
4391
|
+
return {
|
|
4392
|
+
valid: false,
|
|
4393
|
+
error: options.emptyMessage || `Expected ${startRule} input`
|
|
4394
|
+
};
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
try {
|
|
4398
|
+
parser.getAST(normalized, startRule);
|
|
4399
|
+
} catch (error) {
|
|
4400
|
+
return {
|
|
4401
|
+
valid: false,
|
|
4402
|
+
error: options.parseMessage || error.message
|
|
4403
|
+
};
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
if (options.semanticCheck) {
|
|
4407
|
+
const semanticError = options.semanticCheck(value, normalized);
|
|
4408
|
+
if (semanticError) {
|
|
4409
|
+
return {
|
|
4410
|
+
valid: false,
|
|
4411
|
+
error: semanticError
|
|
4412
|
+
};
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
return { valid: true };
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
static _getParser() {
|
|
4420
|
+
if (!ParserCache) {
|
|
4421
|
+
ParserCache = new Parser(ValidationRules, { debug: false });
|
|
4422
|
+
}
|
|
4423
|
+
|
|
4424
|
+
return ParserCache;
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
static _serializeString(value) {
|
|
4428
|
+
if (typeof value !== 'string') {
|
|
4429
|
+
return null;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
return JSON.stringify(value);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
static _serializeNumber(value) {
|
|
4436
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
4437
|
+
return null;
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4440
|
+
return String(value);
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
static _serializeBoolean(value) {
|
|
4444
|
+
if (typeof value !== 'boolean') {
|
|
4445
|
+
return null;
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
return value ? 'true' : 'false';
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
static _serializeTimeValue(value) {
|
|
4452
|
+
if (typeof value !== 'string') {
|
|
4453
|
+
return null;
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
return value.trim();
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
static _serializeTimePeriod(value) {
|
|
4460
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
4461
|
+
return null;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
return `${value.from.trim()} TO ${value.to.trim()}`;
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
static _serializeTimePeriodAgo(value) {
|
|
4468
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
4469
|
+
return null;
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4472
|
+
if (!Array.isArray(value.ago) || value.ago.length !== 2) {
|
|
4473
|
+
return null;
|
|
4474
|
+
}
|
|
4475
|
+
|
|
4476
|
+
const [amount, unit] = value.ago;
|
|
4477
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || typeof unit !== 'string') {
|
|
4478
|
+
return null;
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
return `${value.from.trim()} TO ${value.to.trim()} AGO ${amount} ${unit.trim()}`;
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
static _serializeNumberTime(value) {
|
|
4485
|
+
if (typeof value !== 'string') {
|
|
4486
|
+
return null;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
return value.trim();
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
static _serializeJsonObject(value) {
|
|
4493
|
+
if (!VariableValidate._isJsonObject(value)) {
|
|
4494
|
+
return null;
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
return JSON.stringify(value);
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
static _serializeTypedArray(value, predicate) {
|
|
4501
|
+
if (!Array.isArray(value) || !value.every(predicate)) {
|
|
4502
|
+
return null;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
return JSON.stringify(value);
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
static _serializeObjectArray(value) {
|
|
4509
|
+
if (!Array.isArray(value) || !value.every(item => VariableValidate._isJsonObject(item))) {
|
|
4510
|
+
return null;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
return JSON.stringify(value);
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4516
|
+
static _isPlainObject(value) {
|
|
4517
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
static _isJsonObject(value) {
|
|
4521
|
+
if (!VariableValidate._isPlainObject(value)) {
|
|
4522
|
+
return false;
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4525
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
static _isJsonValue(value) {
|
|
4529
|
+
if (value === null) {
|
|
4530
|
+
return true;
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4533
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
4534
|
+
return true;
|
|
4535
|
+
}
|
|
4536
|
+
|
|
4537
|
+
if (typeof value === 'number') {
|
|
4538
|
+
return Number.isFinite(value);
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
if (Array.isArray(value)) {
|
|
4542
|
+
return value.every(entry => VariableValidate._isJsonValue(entry));
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
if (VariableValidate._isPlainObject(value)) {
|
|
4546
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
4547
|
+
}
|
|
4548
|
+
|
|
4549
|
+
return false;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
static _validateTimeOfDay(value) {
|
|
4553
|
+
if (typeof value !== 'string') {
|
|
4554
|
+
return 'Time value must be a string in HH:MM format';
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
const match = value.trim().match(/^(\d{1,2}):(\d{1,2})$/);
|
|
4558
|
+
if (!match) {
|
|
4559
|
+
return 'Time value must be a string in HH:MM format';
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
const hours = Number(match[1]);
|
|
4563
|
+
const minutes = Number(match[2]);
|
|
4564
|
+
|
|
4565
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
4566
|
+
return 'Time value must contain hours 0-23 and minutes 0-59';
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
return null;
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
VariableValidate.VariableTypes = VariableTypes.slice();
|
|
4574
|
+
VariableValidate.validators = Object.freeze({
|
|
4575
|
+
'string': (value) => VariableValidate._validateWithRule(value, 'string_atom', {
|
|
4576
|
+
normalize: VariableValidate._serializeString,
|
|
4577
|
+
emptyMessage: 'String variables must be JavaScript strings',
|
|
4578
|
+
parseMessage: 'String variables must serialize to a valid quoted string literal'
|
|
4579
|
+
}),
|
|
4580
|
+
'number': (value) => VariableValidate._validateWithRule(value, 'number', {
|
|
4581
|
+
normalize: VariableValidate._serializeNumber,
|
|
4582
|
+
emptyMessage: 'Number variables must be finite numbers',
|
|
4583
|
+
parseMessage: 'Number variables must serialize to a valid numeric literal'
|
|
4584
|
+
}),
|
|
4585
|
+
'boolean': (value) => VariableValidate._validateWithRule(value, 'boolean_atom', {
|
|
4586
|
+
normalize: VariableValidate._serializeBoolean,
|
|
4587
|
+
emptyMessage: 'Boolean variables must be JavaScript booleans',
|
|
4588
|
+
parseMessage: 'Boolean variables must serialize to a valid boolean literal'
|
|
4589
|
+
}),
|
|
4590
|
+
'object': (value) => VariableValidate._validateWithRule(value, 'object_atom', {
|
|
4591
|
+
normalize: VariableValidate._serializeJsonObject,
|
|
4592
|
+
emptyMessage: 'Object variables must be plain JSON-compatible objects',
|
|
4593
|
+
parseMessage: 'Object variables must serialize to valid JSON object syntax'
|
|
4594
|
+
}),
|
|
4595
|
+
'time period': (value) => VariableValidate._validateWithRule(value, 'time_period_atom', {
|
|
4596
|
+
normalize: VariableValidate._serializeTimePeriod,
|
|
4597
|
+
emptyMessage: 'Time period variables must be objects with string from/to properties',
|
|
4598
|
+
parseMessage: 'Time period variables must serialize to FROM TO TO syntax',
|
|
4599
|
+
semanticCheck: (rawValue) => {
|
|
4600
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
4601
|
+
if (fromError) return `Invalid time period from value: ${fromError}`;
|
|
4602
|
+
|
|
4603
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
4604
|
+
if (toError) return `Invalid time period to value: ${toError}`;
|
|
4605
|
+
|
|
4606
|
+
return null;
|
|
4607
|
+
}
|
|
4608
|
+
}),
|
|
4609
|
+
'time period ago': (value) => VariableValidate._validateWithRule(value, 'time_period_ago_atom', {
|
|
4610
|
+
normalize: VariableValidate._serializeTimePeriodAgo,
|
|
4611
|
+
emptyMessage: 'Time period ago variables must be objects with from, to, and ago properties',
|
|
4612
|
+
parseMessage: 'Time period ago variables must serialize to FROM TO TO AGO AMOUNT UNIT syntax',
|
|
4613
|
+
semanticCheck: (rawValue) => {
|
|
4614
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
4615
|
+
if (fromError) return `Invalid time period ago from value: ${fromError}`;
|
|
4616
|
+
|
|
4617
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
4618
|
+
if (toError) return `Invalid time period ago to value: ${toError}`;
|
|
4619
|
+
|
|
4620
|
+
return null;
|
|
4621
|
+
}
|
|
4622
|
+
}),
|
|
4623
|
+
'time value': (value) => VariableValidate._validateWithRule(value, 'time_value_atom', {
|
|
4624
|
+
normalize: VariableValidate._serializeTimeValue,
|
|
4625
|
+
emptyMessage: 'Time value variables must be strings in HH:MM format',
|
|
4626
|
+
parseMessage: 'Time value variables must serialize to HH:MM syntax',
|
|
4627
|
+
semanticCheck: (rawValue) => VariableValidate._validateTimeOfDay(rawValue)
|
|
4628
|
+
}),
|
|
4629
|
+
'number time': (value) => VariableValidate._validateWithRule(value, 'number_time', {
|
|
4630
|
+
normalize: VariableValidate._serializeNumberTime,
|
|
4631
|
+
emptyMessage: 'Number time variables must be strings like "2 hours"',
|
|
4632
|
+
parseMessage: 'Number time variables must serialize to NUMBER UNIT syntax'
|
|
4633
|
+
}),
|
|
4634
|
+
'string array': (value) => VariableValidate._validateWithRule(value, 'string_array', {
|
|
4635
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'string'),
|
|
4636
|
+
emptyMessage: 'String array variables must be arrays of strings',
|
|
4637
|
+
parseMessage: 'String array variables must serialize to a valid string array literal'
|
|
4638
|
+
}),
|
|
4639
|
+
'number array': (value) => VariableValidate._validateWithRule(value, 'number_array', {
|
|
4640
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'number' && Number.isFinite(item)),
|
|
4641
|
+
emptyMessage: 'Number array variables must be arrays of finite numbers',
|
|
4642
|
+
parseMessage: 'Number array variables must serialize to a valid number array literal'
|
|
4643
|
+
}),
|
|
4644
|
+
'boolean array': (value) => VariableValidate._validateWithRule(value, 'boolean_array', {
|
|
4645
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'boolean'),
|
|
4646
|
+
emptyMessage: 'Boolean array variables must be arrays of booleans',
|
|
4647
|
+
parseMessage: 'Boolean array variables must serialize to a valid boolean array literal'
|
|
4648
|
+
}),
|
|
4649
|
+
'object array': (value) => VariableValidate._validateWithRule(value, 'object_array', {
|
|
4650
|
+
normalize: VariableValidate._serializeObjectArray,
|
|
4651
|
+
emptyMessage: 'Object array variables must be arrays of plain JSON-compatible objects',
|
|
4652
|
+
parseMessage: 'Object array variables must serialize to a valid object array literal'
|
|
4653
|
+
})
|
|
4654
|
+
});
|
|
4655
|
+
|
|
4656
|
+
module.exports = VariableValidate;
|
|
4657
|
+
},{"./RuleTemplate.ebnf":15,"@halleyassist/rule-parser":2,"ebnf":13}]},{},[14])(14)
|
|
4236
4658
|
});
|
package/index.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export interface VariableValue {
|
|
|
14
14
|
from: string;
|
|
15
15
|
to: string;
|
|
16
16
|
ago?: [number, string];
|
|
17
|
-
};
|
|
18
|
-
type?: 'string' | 'number' | 'boolean' | 'object' | 'time period' | 'time period ago' | 'time value' | 'string array' | 'number array' | 'boolean array' | 'object array';
|
|
17
|
+
} | Record<string, any> | string[] | number[] | boolean[] | Record<string, any>[];
|
|
18
|
+
type?: 'string' | 'number' | 'boolean' | 'object' | 'time period' | 'time period ago' | 'time value' | 'number time' | 'string array' | 'number array' | 'boolean array' | 'object array';
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface Variables {
|
|
@@ -27,6 +27,11 @@ export interface ValidationResult {
|
|
|
27
27
|
errors: string[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export interface VariableValidationResult {
|
|
31
|
+
valid: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export interface ASTNode {
|
|
31
36
|
type: string;
|
|
32
37
|
text?: string;
|
|
@@ -139,6 +144,14 @@ export class VariableTemplate {
|
|
|
139
144
|
format(variableData: VariableValue | Variables): VariableValue;
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
export class VariableValidate {
|
|
148
|
+
static VariableTypes: string[];
|
|
149
|
+
static validators: Record<string, (value: any) => VariableValidationResult>;
|
|
150
|
+
static validate(variableData: VariableValue): VariableValidationResult;
|
|
151
|
+
static validateValue(type: string, value: any): VariableValidationResult;
|
|
152
|
+
static isValid(type: string, value: any): boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
142
155
|
export const ParserRules: any[];
|
|
143
156
|
export const VariableTypes: string[];
|
|
144
157
|
export const TemplateFilters: TemplateFiltersType;
|
package/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
const RuleTemplate = require('./src/RuleTemplate');
|
|
2
2
|
const GeneralTemplate = require('./src/GeneralTemplate');
|
|
3
3
|
const VariableTemplate = require('./src/VariableTemplate');
|
|
4
|
+
const VariableValidate = require('./src/VariableValidate');
|
|
4
5
|
|
|
5
6
|
module.exports.RuleTemplate = RuleTemplate;
|
|
6
7
|
module.exports.ParserRules = RuleTemplate.ParserRules;
|
|
7
8
|
module.exports.VariableTypes = RuleTemplate.VariableTypes;
|
|
8
9
|
module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
|
|
10
|
+
module.exports.VariableValidate = VariableValidate;
|
|
9
11
|
module.exports.GeneralTemplate = GeneralTemplate;
|
|
10
12
|
module.exports.VariableTemplate = VariableTemplate;
|
package/package.json
CHANGED
package/src/RuleTemplate.ebnf.js
CHANGED
|
@@ -29,6 +29,23 @@ const grammar = `
|
|
|
29
29
|
dow_atom ::= dow | template_value
|
|
30
30
|
between_time_only_atom ::= between_time_only | template_value
|
|
31
31
|
between_tod_only_atom ::= between_tod_only | template_value
|
|
32
|
+
|
|
33
|
+
string_atom ::= string
|
|
34
|
+
boolean_atom ::= false | true
|
|
35
|
+
time_value_atom ::= number_tod
|
|
36
|
+
time_period_atom ::= time_value_atom WS* "TO" WS* time_value_atom
|
|
37
|
+
time_period_ago_atom ::= time_value_atom WS* "TO" WS* time_value_atom WS+ AGO WS+ number WS+ unit
|
|
38
|
+
|
|
39
|
+
object_atom ::= json_object
|
|
40
|
+
json_value ::= string | number | false | true | null | json_array | json_object
|
|
41
|
+
json_member ::= string NAME_SEPARATOR json_value
|
|
42
|
+
json_object ::= BEGIN_OBJECT (json_member (VALUE_SEPARATOR json_member)*)? END_OBJECT
|
|
43
|
+
json_array ::= BEGIN_ARRAY (json_value (VALUE_SEPARATOR json_value)*)? END_ARRAY
|
|
44
|
+
|
|
45
|
+
string_array ::= BEGIN_ARRAY (string (VALUE_SEPARATOR string)*)? END_ARRAY
|
|
46
|
+
number_array ::= BEGIN_ARRAY (number (VALUE_SEPARATOR number)*)? END_ARRAY
|
|
47
|
+
boolean_array ::= BEGIN_ARRAY (boolean_atom (VALUE_SEPARATOR boolean_atom)*)? END_ARRAY
|
|
48
|
+
object_array ::= BEGIN_ARRAY (json_object (VALUE_SEPARATOR json_object)*)? END_ARRAY
|
|
32
49
|
`
|
|
33
50
|
|
|
34
51
|
module.exports = Grammars.W3C.getRules(grammar);
|
package/src/RuleTemplate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Note: We are coupled closely with the ebnf grammar structure of rule-parser
|
|
2
2
|
const TemplateGrammar = require('./RuleTemplate.ebnf'),
|
|
3
3
|
TemplateFilters = require('./TemplateFilters'),
|
|
4
|
+
VariableValidate = require('./VariableValidate'),
|
|
4
5
|
RuleParser = require('@halleyassist/rule-parser'),
|
|
5
6
|
RuleParserRules = RuleParser.ParserRules,
|
|
6
7
|
{Parser} = require('ebnf');
|
|
@@ -15,6 +16,7 @@ const VariableTypes = [
|
|
|
15
16
|
'time period',
|
|
16
17
|
'time period ago',
|
|
17
18
|
'time value',
|
|
19
|
+
'number time',
|
|
18
20
|
'string array',
|
|
19
21
|
'number array',
|
|
20
22
|
'boolean array',
|
|
@@ -28,6 +30,7 @@ const AllowedTypeMapping = {
|
|
|
28
30
|
'time period': ['time_period_atom'],
|
|
29
31
|
'time period ago': ['time_period_atom'],
|
|
30
32
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
33
|
+
'number time': ['number_atom'],
|
|
31
34
|
'string array': ['string_array'],
|
|
32
35
|
'number array': ['number_array'],
|
|
33
36
|
'boolean array': ['boolean_array'],
|
|
@@ -245,6 +248,14 @@ class RuleTemplate {
|
|
|
245
248
|
// Validate type if provided
|
|
246
249
|
if (type && !VariableTypes.includes(type)) {
|
|
247
250
|
errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (type) {
|
|
255
|
+
const validation = VariableValidate.validate(varData);
|
|
256
|
+
if (!validation.valid) {
|
|
257
|
+
errors.push(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
258
|
+
}
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
|
|
@@ -365,6 +376,11 @@ class RuleTemplate {
|
|
|
365
376
|
}
|
|
366
377
|
}
|
|
367
378
|
|
|
379
|
+
const validation = VariableValidate.validate(varData);
|
|
380
|
+
if (!validation.valid) {
|
|
381
|
+
throw new Error(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
368
384
|
return this._serializeVarData(varData, varName);
|
|
369
385
|
}
|
|
370
386
|
|
|
@@ -395,6 +411,10 @@ class RuleTemplate {
|
|
|
395
411
|
return ret;
|
|
396
412
|
}
|
|
397
413
|
|
|
414
|
+
if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
|
|
415
|
+
return JSON.stringify(value);
|
|
416
|
+
}
|
|
417
|
+
|
|
398
418
|
return String(value);
|
|
399
419
|
}
|
|
400
420
|
|
|
@@ -421,5 +441,6 @@ class RuleTemplate {
|
|
|
421
441
|
RuleTemplate.ParserRules = ParserRules;
|
|
422
442
|
RuleTemplate.VariableTypes = VariableTypes;
|
|
423
443
|
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
444
|
+
RuleTemplate.VariableValidate = VariableValidate;
|
|
424
445
|
|
|
425
446
|
module.exports = RuleTemplate;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
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"]]}]
|
|
1
|
+
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"]]},{"name":"string_atom","bnf":[["string"]]},{"name":"boolean_atom","bnf":[["false"],["true"]]},{"name":"time_value_atom","bnf":[["number_tod"]]},{"name":"time_period_atom","bnf":[["time_value_atom","WS*","\"TO\"","WS*","time_value_atom"]]},{"name":"time_period_ago_atom","bnf":[["time_value_atom","WS*","\"TO\"","WS*","time_value_atom","WS+","AGO","WS+","number","WS+","unit"]]},{"name":"object_atom","bnf":[["json_object"]]},{"name":"json_value","bnf":[["string"],["number"],["false"],["true"],["null"],["json_array"],["json_object"]]},{"name":"json_member","bnf":[["string","NAME_SEPARATOR","json_value"]]},{"name":"%json_object[2][2]","bnf":[["VALUE_SEPARATOR","json_member"]],"fragment":true},{"name":"%json_object[2]","bnf":[["json_member","%json_object[2][2]*"]],"fragment":true},{"name":"json_object","bnf":[["BEGIN_OBJECT","%json_object[2]?","END_OBJECT"]]},{"name":"%json_array[2][2]","bnf":[["VALUE_SEPARATOR","json_value"]],"fragment":true},{"name":"%json_array[2]","bnf":[["json_value","%json_array[2][2]*"]],"fragment":true},{"name":"json_array","bnf":[["BEGIN_ARRAY","%json_array[2]?","END_ARRAY"]]},{"name":"%string_array[2][2]","bnf":[["VALUE_SEPARATOR","string"]],"fragment":true},{"name":"%string_array[2]","bnf":[["string","%string_array[2][2]*"]],"fragment":true},{"name":"string_array","bnf":[["BEGIN_ARRAY","%string_array[2]?","END_ARRAY"]]},{"name":"%number_array[2][2]","bnf":[["VALUE_SEPARATOR","number"]],"fragment":true},{"name":"%number_array[2]","bnf":[["number","%number_array[2][2]*"]],"fragment":true},{"name":"number_array","bnf":[["BEGIN_ARRAY","%number_array[2]?","END_ARRAY"]]},{"name":"%boolean_array[2][2]","bnf":[["VALUE_SEPARATOR","boolean_atom"]],"fragment":true},{"name":"%boolean_array[2]","bnf":[["boolean_atom","%boolean_array[2][2]*"]],"fragment":true},{"name":"boolean_array","bnf":[["BEGIN_ARRAY","%boolean_array[2]?","END_ARRAY"]]},{"name":"%object_array[2][2]","bnf":[["VALUE_SEPARATOR","json_object"]],"fragment":true},{"name":"%object_array[2]","bnf":[["json_object","%object_array[2][2]*"]],"fragment":true},{"name":"object_array","bnf":[["BEGIN_ARRAY","%object_array[2]?","END_ARRAY"]]}]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Note: We are coupled closely with the ebnf grammar structure of rule-parser
|
|
2
2
|
const TemplateGrammar = require('./RuleTemplate.production.ebnf.js'),
|
|
3
3
|
TemplateFilters = require('./TemplateFilters'),
|
|
4
|
+
VariableValidate = require('./VariableValidate'),
|
|
4
5
|
RuleParser = require('@halleyassist/rule-parser'),
|
|
5
6
|
RuleParserRules = RuleParser.ParserRules,
|
|
6
7
|
{Parser} = require('ebnf');
|
|
@@ -15,6 +16,7 @@ const VariableTypes = [
|
|
|
15
16
|
'time period',
|
|
16
17
|
'time period ago',
|
|
17
18
|
'time value',
|
|
19
|
+
'number time',
|
|
18
20
|
'string array',
|
|
19
21
|
'number array',
|
|
20
22
|
'boolean array',
|
|
@@ -28,6 +30,7 @@ const AllowedTypeMapping = {
|
|
|
28
30
|
'time period': ['time_period_atom'],
|
|
29
31
|
'time period ago': ['time_period_atom'],
|
|
30
32
|
'time value': ['time_value_atom', 'tod_atom'],
|
|
33
|
+
'number time': ['number_atom'],
|
|
31
34
|
'string array': ['string_array'],
|
|
32
35
|
'number array': ['number_array'],
|
|
33
36
|
'boolean array': ['boolean_array'],
|
|
@@ -245,6 +248,14 @@ class RuleTemplate {
|
|
|
245
248
|
// Validate type if provided
|
|
246
249
|
if (type && !VariableTypes.includes(type)) {
|
|
247
250
|
errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (type) {
|
|
255
|
+
const validation = VariableValidate.validate(varData);
|
|
256
|
+
if (!validation.valid) {
|
|
257
|
+
errors.push(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
258
|
+
}
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
|
|
@@ -365,6 +376,11 @@ class RuleTemplate {
|
|
|
365
376
|
}
|
|
366
377
|
}
|
|
367
378
|
|
|
379
|
+
const validation = VariableValidate.validate(varData);
|
|
380
|
+
if (!validation.valid) {
|
|
381
|
+
throw new Error(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
368
384
|
return this._serializeVarData(varData, varName);
|
|
369
385
|
}
|
|
370
386
|
|
|
@@ -395,6 +411,10 @@ class RuleTemplate {
|
|
|
395
411
|
return ret;
|
|
396
412
|
}
|
|
397
413
|
|
|
414
|
+
if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
|
|
415
|
+
return JSON.stringify(value);
|
|
416
|
+
}
|
|
417
|
+
|
|
398
418
|
return String(value);
|
|
399
419
|
}
|
|
400
420
|
|
|
@@ -421,5 +441,6 @@ class RuleTemplate {
|
|
|
421
441
|
RuleTemplate.ParserRules = ParserRules;
|
|
422
442
|
RuleTemplate.VariableTypes = VariableTypes;
|
|
423
443
|
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
444
|
+
RuleTemplate.VariableValidate = VariableValidate;
|
|
424
445
|
|
|
425
446
|
module.exports = RuleTemplate;
|
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,345 @@
|
|
|
1
|
+
const { Parser } = require('ebnf');
|
|
2
|
+
const TemplateGrammar = require('./RuleTemplate.ebnf');
|
|
3
|
+
const RuleParser = require('@halleyassist/rule-parser');
|
|
4
|
+
const RuleParserRules = RuleParser.ParserRules;
|
|
5
|
+
|
|
6
|
+
const VariableTypes = [
|
|
7
|
+
'string',
|
|
8
|
+
'number',
|
|
9
|
+
'boolean',
|
|
10
|
+
'object',
|
|
11
|
+
'time period',
|
|
12
|
+
'time period ago',
|
|
13
|
+
'time value',
|
|
14
|
+
'number time',
|
|
15
|
+
'string array',
|
|
16
|
+
'number array',
|
|
17
|
+
'boolean array',
|
|
18
|
+
'object array'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
let ParserCache = null;
|
|
22
|
+
|
|
23
|
+
const ValidationRules = [...RuleParserRules];
|
|
24
|
+
for (const rule of TemplateGrammar) {
|
|
25
|
+
const idx = ValidationRules.findIndex(existingRule => existingRule.name === rule.name);
|
|
26
|
+
if (idx !== -1) {
|
|
27
|
+
ValidationRules[idx] = rule;
|
|
28
|
+
} else {
|
|
29
|
+
ValidationRules.push(rule);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class VariableValidate {
|
|
34
|
+
static validate(variableData) {
|
|
35
|
+
if (!variableData || typeof variableData !== 'object' || Array.isArray(variableData)) {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
error: 'Variable data must be an object with value and type properties'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!Object.prototype.hasOwnProperty.call(variableData, 'type')) {
|
|
43
|
+
return {
|
|
44
|
+
valid: false,
|
|
45
|
+
error: 'Variable data must include a type property'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return VariableValidate.validateValue(variableData.type, variableData.value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
static validateValue(type, value) {
|
|
53
|
+
const validator = VariableValidate.validators[type];
|
|
54
|
+
if (!validator) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
error: `Unsupported variable type '${type}'`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return validator(value);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
valid: false,
|
|
66
|
+
error: error.message
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static isValid(type, value) {
|
|
72
|
+
return VariableValidate.validateValue(type, value).valid;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static _validateWithRule(value, startRule, options = {}) {
|
|
76
|
+
const parser = VariableValidate._getParser();
|
|
77
|
+
const normalized = options.normalize ? options.normalize(value) : value;
|
|
78
|
+
|
|
79
|
+
if (typeof normalized !== 'string' || !normalized.length) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
error: options.emptyMessage || `Expected ${startRule} input`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
parser.getAST(normalized, startRule);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: options.parseMessage || error.message
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (options.semanticCheck) {
|
|
96
|
+
const semanticError = options.semanticCheck(value, normalized);
|
|
97
|
+
if (semanticError) {
|
|
98
|
+
return {
|
|
99
|
+
valid: false,
|
|
100
|
+
error: semanticError
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { valid: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static _getParser() {
|
|
109
|
+
if (!ParserCache) {
|
|
110
|
+
ParserCache = new Parser(ValidationRules, { debug: false });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return ParserCache;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static _serializeString(value) {
|
|
117
|
+
if (typeof value !== 'string') {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return JSON.stringify(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static _serializeNumber(value) {
|
|
125
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return String(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
static _serializeBoolean(value) {
|
|
133
|
+
if (typeof value !== 'boolean') {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return value ? 'true' : 'false';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static _serializeTimeValue(value) {
|
|
141
|
+
if (typeof value !== 'string') {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return value.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static _serializeTimePeriod(value) {
|
|
149
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${value.from.trim()} TO ${value.to.trim()}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static _serializeTimePeriodAgo(value) {
|
|
157
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!Array.isArray(value.ago) || value.ago.length !== 2) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const [amount, unit] = value.ago;
|
|
166
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || typeof unit !== 'string') {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return `${value.from.trim()} TO ${value.to.trim()} AGO ${amount} ${unit.trim()}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static _serializeNumberTime(value) {
|
|
174
|
+
if (typeof value !== 'string') {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return value.trim();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static _serializeJsonObject(value) {
|
|
182
|
+
if (!VariableValidate._isJsonObject(value)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return JSON.stringify(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static _serializeTypedArray(value, predicate) {
|
|
190
|
+
if (!Array.isArray(value) || !value.every(predicate)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return JSON.stringify(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
static _serializeObjectArray(value) {
|
|
198
|
+
if (!Array.isArray(value) || !value.every(item => VariableValidate._isJsonObject(item))) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return JSON.stringify(value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static _isPlainObject(value) {
|
|
206
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static _isJsonObject(value) {
|
|
210
|
+
if (!VariableValidate._isPlainObject(value)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
static _isJsonValue(value) {
|
|
218
|
+
if (value === null) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof value === 'number') {
|
|
227
|
+
return Number.isFinite(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.every(entry => VariableValidate._isJsonValue(entry));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (VariableValidate._isPlainObject(value)) {
|
|
235
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static _validateTimeOfDay(value) {
|
|
242
|
+
if (typeof value !== 'string') {
|
|
243
|
+
return 'Time value must be a string in HH:MM format';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const match = value.trim().match(/^(\d{1,2}):(\d{1,2})$/);
|
|
247
|
+
if (!match) {
|
|
248
|
+
return 'Time value must be a string in HH:MM format';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const hours = Number(match[1]);
|
|
252
|
+
const minutes = Number(match[2]);
|
|
253
|
+
|
|
254
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
255
|
+
return 'Time value must contain hours 0-23 and minutes 0-59';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
VariableValidate.VariableTypes = VariableTypes.slice();
|
|
263
|
+
VariableValidate.validators = Object.freeze({
|
|
264
|
+
'string': (value) => VariableValidate._validateWithRule(value, 'string_atom', {
|
|
265
|
+
normalize: VariableValidate._serializeString,
|
|
266
|
+
emptyMessage: 'String variables must be JavaScript strings',
|
|
267
|
+
parseMessage: 'String variables must serialize to a valid quoted string literal'
|
|
268
|
+
}),
|
|
269
|
+
'number': (value) => VariableValidate._validateWithRule(value, 'number', {
|
|
270
|
+
normalize: VariableValidate._serializeNumber,
|
|
271
|
+
emptyMessage: 'Number variables must be finite numbers',
|
|
272
|
+
parseMessage: 'Number variables must serialize to a valid numeric literal'
|
|
273
|
+
}),
|
|
274
|
+
'boolean': (value) => VariableValidate._validateWithRule(value, 'boolean_atom', {
|
|
275
|
+
normalize: VariableValidate._serializeBoolean,
|
|
276
|
+
emptyMessage: 'Boolean variables must be JavaScript booleans',
|
|
277
|
+
parseMessage: 'Boolean variables must serialize to a valid boolean literal'
|
|
278
|
+
}),
|
|
279
|
+
'object': (value) => VariableValidate._validateWithRule(value, 'object_atom', {
|
|
280
|
+
normalize: VariableValidate._serializeJsonObject,
|
|
281
|
+
emptyMessage: 'Object variables must be plain JSON-compatible objects',
|
|
282
|
+
parseMessage: 'Object variables must serialize to valid JSON object syntax'
|
|
283
|
+
}),
|
|
284
|
+
'time period': (value) => VariableValidate._validateWithRule(value, 'time_period_atom', {
|
|
285
|
+
normalize: VariableValidate._serializeTimePeriod,
|
|
286
|
+
emptyMessage: 'Time period variables must be objects with string from/to properties',
|
|
287
|
+
parseMessage: 'Time period variables must serialize to FROM TO TO syntax',
|
|
288
|
+
semanticCheck: (rawValue) => {
|
|
289
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
290
|
+
if (fromError) return `Invalid time period from value: ${fromError}`;
|
|
291
|
+
|
|
292
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
293
|
+
if (toError) return `Invalid time period to value: ${toError}`;
|
|
294
|
+
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}),
|
|
298
|
+
'time period ago': (value) => VariableValidate._validateWithRule(value, 'time_period_ago_atom', {
|
|
299
|
+
normalize: VariableValidate._serializeTimePeriodAgo,
|
|
300
|
+
emptyMessage: 'Time period ago variables must be objects with from, to, and ago properties',
|
|
301
|
+
parseMessage: 'Time period ago variables must serialize to FROM TO TO AGO AMOUNT UNIT syntax',
|
|
302
|
+
semanticCheck: (rawValue) => {
|
|
303
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
304
|
+
if (fromError) return `Invalid time period ago from value: ${fromError}`;
|
|
305
|
+
|
|
306
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
307
|
+
if (toError) return `Invalid time period ago to value: ${toError}`;
|
|
308
|
+
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}),
|
|
312
|
+
'time value': (value) => VariableValidate._validateWithRule(value, 'time_value_atom', {
|
|
313
|
+
normalize: VariableValidate._serializeTimeValue,
|
|
314
|
+
emptyMessage: 'Time value variables must be strings in HH:MM format',
|
|
315
|
+
parseMessage: 'Time value variables must serialize to HH:MM syntax',
|
|
316
|
+
semanticCheck: (rawValue) => VariableValidate._validateTimeOfDay(rawValue)
|
|
317
|
+
}),
|
|
318
|
+
'number time': (value) => VariableValidate._validateWithRule(value, 'number_time', {
|
|
319
|
+
normalize: VariableValidate._serializeNumberTime,
|
|
320
|
+
emptyMessage: 'Number time variables must be strings like "2 hours"',
|
|
321
|
+
parseMessage: 'Number time variables must serialize to NUMBER UNIT syntax'
|
|
322
|
+
}),
|
|
323
|
+
'string array': (value) => VariableValidate._validateWithRule(value, 'string_array', {
|
|
324
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'string'),
|
|
325
|
+
emptyMessage: 'String array variables must be arrays of strings',
|
|
326
|
+
parseMessage: 'String array variables must serialize to a valid string array literal'
|
|
327
|
+
}),
|
|
328
|
+
'number array': (value) => VariableValidate._validateWithRule(value, 'number_array', {
|
|
329
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'number' && Number.isFinite(item)),
|
|
330
|
+
emptyMessage: 'Number array variables must be arrays of finite numbers',
|
|
331
|
+
parseMessage: 'Number array variables must serialize to a valid number array literal'
|
|
332
|
+
}),
|
|
333
|
+
'boolean array': (value) => VariableValidate._validateWithRule(value, 'boolean_array', {
|
|
334
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'boolean'),
|
|
335
|
+
emptyMessage: 'Boolean array variables must be arrays of booleans',
|
|
336
|
+
parseMessage: 'Boolean array variables must serialize to a valid boolean array literal'
|
|
337
|
+
}),
|
|
338
|
+
'object array': (value) => VariableValidate._validateWithRule(value, 'object_array', {
|
|
339
|
+
normalize: VariableValidate._serializeObjectArray,
|
|
340
|
+
emptyMessage: 'Object array variables must be arrays of plain JSON-compatible objects',
|
|
341
|
+
parseMessage: 'Object array variables must serialize to a valid object array literal'
|
|
342
|
+
})
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
module.exports = VariableValidate;
|