@halleyassist/rule-templater 0.0.14 → 0.0.16
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 +20 -0
- package/dist/rule-templater.browser.js +477 -5
- package/index.d.ts +16 -2
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/RuleTemplate.ebnf.js +17 -0
- package/src/RuleTemplate.js +19 -0
- package/src/RuleTemplate.production.ebnf.js +1 -1
- package/src/RuleTemplate.production.js +19 -0
- package/src/VariableValidate.js +399 -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,6 +335,7 @@ 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`
|
|
@@ -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');
|
|
@@ -3897,6 +3951,14 @@ class RuleTemplate {
|
|
|
3897
3951
|
// Validate type if provided
|
|
3898
3952
|
if (type && !VariableTypes.includes(type)) {
|
|
3899
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
|
+
}
|
|
3900
3962
|
}
|
|
3901
3963
|
}
|
|
3902
3964
|
|
|
@@ -4017,6 +4079,11 @@ class RuleTemplate {
|
|
|
4017
4079
|
}
|
|
4018
4080
|
}
|
|
4019
4081
|
|
|
4082
|
+
const validation = VariableValidate.validate(varData);
|
|
4083
|
+
if (!validation.valid) {
|
|
4084
|
+
throw new Error(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4020
4087
|
return this._serializeVarData(varData, varName);
|
|
4021
4088
|
}
|
|
4022
4089
|
|
|
@@ -4047,6 +4114,10 @@ class RuleTemplate {
|
|
|
4047
4114
|
return ret;
|
|
4048
4115
|
}
|
|
4049
4116
|
|
|
4117
|
+
if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
|
|
4118
|
+
return JSON.stringify(value);
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4050
4121
|
return String(value);
|
|
4051
4122
|
}
|
|
4052
4123
|
|
|
@@ -4073,10 +4144,11 @@ class RuleTemplate {
|
|
|
4073
4144
|
RuleTemplate.ParserRules = ParserRules;
|
|
4074
4145
|
RuleTemplate.VariableTypes = VariableTypes;
|
|
4075
4146
|
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
4147
|
+
RuleTemplate.VariableValidate = VariableValidate;
|
|
4076
4148
|
|
|
4077
4149
|
module.exports = RuleTemplate;
|
|
4078
4150
|
|
|
4079
|
-
},{"./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){
|
|
4080
4152
|
/*
|
|
4081
4153
|
Template filters are functions that transform variable values.
|
|
4082
4154
|
They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
|
|
@@ -4236,5 +4308,405 @@ const TemplateFilters = {
|
|
|
4236
4308
|
}
|
|
4237
4309
|
|
|
4238
4310
|
module.exports = TemplateFilters;
|
|
4239
|
-
},{}]
|
|
4311
|
+
},{}],19:[function(require,module,exports){
|
|
4312
|
+
const { Parser } = require('ebnf');
|
|
4313
|
+
const TemplateGrammar = require('./RuleTemplate.ebnf');
|
|
4314
|
+
const TemplateFilters = require('./TemplateFilters');
|
|
4315
|
+
const RuleParser = require('@halleyassist/rule-parser');
|
|
4316
|
+
const RuleParserRules = RuleParser.ParserRules;
|
|
4317
|
+
|
|
4318
|
+
const VariableTypes = [
|
|
4319
|
+
'string',
|
|
4320
|
+
'number',
|
|
4321
|
+
'boolean',
|
|
4322
|
+
'object',
|
|
4323
|
+
'time period',
|
|
4324
|
+
'time period ago',
|
|
4325
|
+
'time value',
|
|
4326
|
+
'number time',
|
|
4327
|
+
'string array',
|
|
4328
|
+
'number array',
|
|
4329
|
+
'boolean array',
|
|
4330
|
+
'object array'
|
|
4331
|
+
];
|
|
4332
|
+
|
|
4333
|
+
let ParserCache = null;
|
|
4334
|
+
|
|
4335
|
+
const ValidationRules = [...RuleParserRules];
|
|
4336
|
+
for (const rule of TemplateGrammar) {
|
|
4337
|
+
const idx = ValidationRules.findIndex(existingRule => existingRule.name === rule.name);
|
|
4338
|
+
if (idx !== -1) {
|
|
4339
|
+
ValidationRules[idx] = rule;
|
|
4340
|
+
} else {
|
|
4341
|
+
ValidationRules.push(rule);
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
class VariableValidate {
|
|
4346
|
+
static validate(variableData) {
|
|
4347
|
+
if (!variableData || typeof variableData !== 'object' || Array.isArray(variableData)) {
|
|
4348
|
+
return {
|
|
4349
|
+
valid: false,
|
|
4350
|
+
error: 'Variable data must be an object with value and type properties'
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
if (!Object.prototype.hasOwnProperty.call(variableData, 'type')) {
|
|
4355
|
+
return {
|
|
4356
|
+
valid: false,
|
|
4357
|
+
error: 'Variable data must include a type property'
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
|
|
4361
|
+
let normalizedVarData;
|
|
4362
|
+
try {
|
|
4363
|
+
normalizedVarData = VariableValidate._normalizeVarData(variableData);
|
|
4364
|
+
} catch (error) {
|
|
4365
|
+
return {
|
|
4366
|
+
valid: false,
|
|
4367
|
+
error: error.message
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
return VariableValidate.validateValue(normalizedVarData.type, normalizedVarData.value);
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
static validateValue(type, value) {
|
|
4375
|
+
const validator = VariableValidate.validators[type];
|
|
4376
|
+
if (!validator) {
|
|
4377
|
+
return {
|
|
4378
|
+
valid: false,
|
|
4379
|
+
error: `Unsupported variable type '${type}'`
|
|
4380
|
+
};
|
|
4381
|
+
}
|
|
4382
|
+
|
|
4383
|
+
try {
|
|
4384
|
+
return validator(value);
|
|
4385
|
+
} catch (error) {
|
|
4386
|
+
return {
|
|
4387
|
+
valid: false,
|
|
4388
|
+
error: error.message
|
|
4389
|
+
};
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
|
|
4393
|
+
static isValid(type, value) {
|
|
4394
|
+
return VariableValidate.validateValue(type, value).valid;
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
static _validateWithRule(value, startRule, options = {}) {
|
|
4398
|
+
const parser = VariableValidate._getParser();
|
|
4399
|
+
const normalized = options.normalize ? options.normalize(value) : value;
|
|
4400
|
+
|
|
4401
|
+
if (typeof normalized !== 'string' || !normalized.length) {
|
|
4402
|
+
return {
|
|
4403
|
+
valid: false,
|
|
4404
|
+
error: options.emptyMessage || `Expected ${startRule} input`
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
try {
|
|
4409
|
+
parser.getAST(normalized, startRule);
|
|
4410
|
+
} catch (error) {
|
|
4411
|
+
return {
|
|
4412
|
+
valid: false,
|
|
4413
|
+
error: options.parseMessage || error.message
|
|
4414
|
+
};
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
if (options.semanticCheck) {
|
|
4418
|
+
const semanticError = options.semanticCheck(value, normalized);
|
|
4419
|
+
if (semanticError) {
|
|
4420
|
+
return {
|
|
4421
|
+
valid: false,
|
|
4422
|
+
error: semanticError
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
return { valid: true };
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
static _getParser() {
|
|
4431
|
+
if (!ParserCache) {
|
|
4432
|
+
ParserCache = new Parser(ValidationRules, { debug: false });
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
return ParserCache;
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
static _normalizeVarData(variableData) {
|
|
4439
|
+
const normalizedVarData = VariableValidate._cloneVarData(variableData);
|
|
4440
|
+
|
|
4441
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedVarData, 'value')) {
|
|
4442
|
+
throw new Error('Variable data must include a value property');
|
|
4443
|
+
}
|
|
4444
|
+
|
|
4445
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedVarData, 'filters')) {
|
|
4446
|
+
return normalizedVarData;
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
if (!Array.isArray(normalizedVarData.filters)) {
|
|
4450
|
+
throw new Error('Variable data filters must be an array');
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
for (const filterName of normalizedVarData.filters) {
|
|
4454
|
+
if (!TemplateFilters[filterName]) {
|
|
4455
|
+
throw new Error(`Unknown filter '${filterName}'`);
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
TemplateFilters[filterName](normalizedVarData);
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
return normalizedVarData;
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
static _cloneVarData(variableData) {
|
|
4465
|
+
const cloned = Object.assign({}, variableData);
|
|
4466
|
+
if (Array.isArray(cloned.filters)) {
|
|
4467
|
+
cloned.filters = cloned.filters.slice();
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
if (cloned.value && typeof cloned.value === 'object') {
|
|
4471
|
+
if (Array.isArray(cloned.value)) {
|
|
4472
|
+
cloned.value = cloned.value.slice();
|
|
4473
|
+
} else {
|
|
4474
|
+
cloned.value = Object.assign({}, cloned.value);
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
return cloned;
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
static _serializeString(value) {
|
|
4482
|
+
if (typeof value !== 'string') {
|
|
4483
|
+
return null;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
return JSON.stringify(value);
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
static _serializeNumber(value) {
|
|
4490
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
4491
|
+
return null;
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
return String(value);
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
static _serializeBoolean(value) {
|
|
4498
|
+
if (typeof value !== 'boolean') {
|
|
4499
|
+
return null;
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4502
|
+
return value ? 'true' : 'false';
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
static _serializeTimeValue(value) {
|
|
4506
|
+
if (typeof value !== 'string') {
|
|
4507
|
+
return null;
|
|
4508
|
+
}
|
|
4509
|
+
|
|
4510
|
+
return value.trim();
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
static _serializeTimePeriod(value) {
|
|
4514
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
4515
|
+
return null;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
return `${value.from.trim()} TO ${value.to.trim()}`;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
static _serializeTimePeriodAgo(value) {
|
|
4522
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
4523
|
+
return null;
|
|
4524
|
+
}
|
|
4525
|
+
|
|
4526
|
+
if (!Array.isArray(value.ago) || value.ago.length !== 2) {
|
|
4527
|
+
return null;
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
const [amount, unit] = value.ago;
|
|
4531
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || typeof unit !== 'string') {
|
|
4532
|
+
return null;
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
return `${value.from.trim()} TO ${value.to.trim()} AGO ${amount} ${unit.trim()}`;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
static _serializeNumberTime(value) {
|
|
4539
|
+
if (typeof value !== 'string') {
|
|
4540
|
+
return null;
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
return value.trim();
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
static _serializeJsonObject(value) {
|
|
4547
|
+
if (!VariableValidate._isJsonObject(value)) {
|
|
4548
|
+
return null;
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
return JSON.stringify(value);
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
static _serializeTypedArray(value, predicate) {
|
|
4555
|
+
if (!Array.isArray(value) || !value.every(predicate)) {
|
|
4556
|
+
return null;
|
|
4557
|
+
}
|
|
4558
|
+
|
|
4559
|
+
return JSON.stringify(value);
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
static _serializeObjectArray(value) {
|
|
4563
|
+
if (!Array.isArray(value) || !value.every(item => VariableValidate._isJsonObject(item))) {
|
|
4564
|
+
return null;
|
|
4565
|
+
}
|
|
4566
|
+
|
|
4567
|
+
return JSON.stringify(value);
|
|
4568
|
+
}
|
|
4569
|
+
|
|
4570
|
+
static _isPlainObject(value) {
|
|
4571
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
static _isJsonObject(value) {
|
|
4575
|
+
if (!VariableValidate._isPlainObject(value)) {
|
|
4576
|
+
return false;
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
4580
|
+
}
|
|
4581
|
+
|
|
4582
|
+
static _isJsonValue(value) {
|
|
4583
|
+
if (value === null) {
|
|
4584
|
+
return true;
|
|
4585
|
+
}
|
|
4586
|
+
|
|
4587
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
4588
|
+
return true;
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
if (typeof value === 'number') {
|
|
4592
|
+
return Number.isFinite(value);
|
|
4593
|
+
}
|
|
4594
|
+
|
|
4595
|
+
if (Array.isArray(value)) {
|
|
4596
|
+
return value.every(entry => VariableValidate._isJsonValue(entry));
|
|
4597
|
+
}
|
|
4598
|
+
|
|
4599
|
+
if (VariableValidate._isPlainObject(value)) {
|
|
4600
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
return false;
|
|
4604
|
+
}
|
|
4605
|
+
|
|
4606
|
+
static _validateTimeOfDay(value) {
|
|
4607
|
+
if (typeof value !== 'string') {
|
|
4608
|
+
return 'Time value must be a string in HH:MM format';
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
const match = value.trim().match(/^(\d{1,2}):(\d{1,2})$/);
|
|
4612
|
+
if (!match) {
|
|
4613
|
+
return 'Time value must be a string in HH:MM format';
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
const hours = Number(match[1]);
|
|
4617
|
+
const minutes = Number(match[2]);
|
|
4618
|
+
|
|
4619
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
4620
|
+
return 'Time value must contain hours 0-23 and minutes 0-59';
|
|
4621
|
+
}
|
|
4622
|
+
|
|
4623
|
+
return null;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
VariableValidate.VariableTypes = VariableTypes.slice();
|
|
4628
|
+
VariableValidate.validators = Object.freeze({
|
|
4629
|
+
'string': (value) => VariableValidate._validateWithRule(value, 'string_atom', {
|
|
4630
|
+
normalize: VariableValidate._serializeString,
|
|
4631
|
+
emptyMessage: 'String variables must be JavaScript strings',
|
|
4632
|
+
parseMessage: 'String variables must serialize to a valid quoted string literal'
|
|
4633
|
+
}),
|
|
4634
|
+
'number': (value) => VariableValidate._validateWithRule(value, 'number', {
|
|
4635
|
+
normalize: VariableValidate._serializeNumber,
|
|
4636
|
+
emptyMessage: 'Number variables must be finite numbers',
|
|
4637
|
+
parseMessage: 'Number variables must serialize to a valid numeric literal'
|
|
4638
|
+
}),
|
|
4639
|
+
'boolean': (value) => VariableValidate._validateWithRule(value, 'boolean_atom', {
|
|
4640
|
+
normalize: VariableValidate._serializeBoolean,
|
|
4641
|
+
emptyMessage: 'Boolean variables must be JavaScript booleans',
|
|
4642
|
+
parseMessage: 'Boolean variables must serialize to a valid boolean literal'
|
|
4643
|
+
}),
|
|
4644
|
+
'object': (value) => VariableValidate._validateWithRule(value, 'object_atom', {
|
|
4645
|
+
normalize: VariableValidate._serializeJsonObject,
|
|
4646
|
+
emptyMessage: 'Object variables must be plain JSON-compatible objects',
|
|
4647
|
+
parseMessage: 'Object variables must serialize to valid JSON object syntax'
|
|
4648
|
+
}),
|
|
4649
|
+
'time period': (value) => VariableValidate._validateWithRule(value, 'time_period_atom', {
|
|
4650
|
+
normalize: VariableValidate._serializeTimePeriod,
|
|
4651
|
+
emptyMessage: 'Time period variables must be objects with string from/to properties',
|
|
4652
|
+
parseMessage: 'Time period variables must serialize to FROM TO TO syntax',
|
|
4653
|
+
semanticCheck: (rawValue) => {
|
|
4654
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
4655
|
+
if (fromError) return `Invalid time period from value: ${fromError}`;
|
|
4656
|
+
|
|
4657
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
4658
|
+
if (toError) return `Invalid time period to value: ${toError}`;
|
|
4659
|
+
|
|
4660
|
+
return null;
|
|
4661
|
+
}
|
|
4662
|
+
}),
|
|
4663
|
+
'time period ago': (value) => VariableValidate._validateWithRule(value, 'time_period_ago_atom', {
|
|
4664
|
+
normalize: VariableValidate._serializeTimePeriodAgo,
|
|
4665
|
+
emptyMessage: 'Time period ago variables must be objects with from, to, and ago properties',
|
|
4666
|
+
parseMessage: 'Time period ago variables must serialize to FROM TO TO AGO AMOUNT UNIT syntax',
|
|
4667
|
+
semanticCheck: (rawValue) => {
|
|
4668
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
4669
|
+
if (fromError) return `Invalid time period ago from value: ${fromError}`;
|
|
4670
|
+
|
|
4671
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
4672
|
+
if (toError) return `Invalid time period ago to value: ${toError}`;
|
|
4673
|
+
|
|
4674
|
+
return null;
|
|
4675
|
+
}
|
|
4676
|
+
}),
|
|
4677
|
+
'time value': (value) => VariableValidate._validateWithRule(value, 'time_value_atom', {
|
|
4678
|
+
normalize: VariableValidate._serializeTimeValue,
|
|
4679
|
+
emptyMessage: 'Time value variables must be strings in HH:MM format',
|
|
4680
|
+
parseMessage: 'Time value variables must serialize to HH:MM syntax',
|
|
4681
|
+
semanticCheck: (rawValue) => VariableValidate._validateTimeOfDay(rawValue)
|
|
4682
|
+
}),
|
|
4683
|
+
'number time': (value) => VariableValidate._validateWithRule(value, 'number_time', {
|
|
4684
|
+
normalize: VariableValidate._serializeNumberTime,
|
|
4685
|
+
emptyMessage: 'Number time variables must be strings like "2 hours"',
|
|
4686
|
+
parseMessage: 'Number time variables must serialize to NUMBER UNIT syntax'
|
|
4687
|
+
}),
|
|
4688
|
+
'string array': (value) => VariableValidate._validateWithRule(value, 'string_array', {
|
|
4689
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'string'),
|
|
4690
|
+
emptyMessage: 'String array variables must be arrays of strings',
|
|
4691
|
+
parseMessage: 'String array variables must serialize to a valid string array literal'
|
|
4692
|
+
}),
|
|
4693
|
+
'number array': (value) => VariableValidate._validateWithRule(value, 'number_array', {
|
|
4694
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'number' && Number.isFinite(item)),
|
|
4695
|
+
emptyMessage: 'Number array variables must be arrays of finite numbers',
|
|
4696
|
+
parseMessage: 'Number array variables must serialize to a valid number array literal'
|
|
4697
|
+
}),
|
|
4698
|
+
'boolean array': (value) => VariableValidate._validateWithRule(value, 'boolean_array', {
|
|
4699
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'boolean'),
|
|
4700
|
+
emptyMessage: 'Boolean array variables must be arrays of booleans',
|
|
4701
|
+
parseMessage: 'Boolean array variables must serialize to a valid boolean array literal'
|
|
4702
|
+
}),
|
|
4703
|
+
'object array': (value) => VariableValidate._validateWithRule(value, 'object_array', {
|
|
4704
|
+
normalize: VariableValidate._serializeObjectArray,
|
|
4705
|
+
emptyMessage: 'Object array variables must be arrays of plain JSON-compatible objects',
|
|
4706
|
+
parseMessage: 'Object array variables must serialize to a valid object array literal'
|
|
4707
|
+
})
|
|
4708
|
+
});
|
|
4709
|
+
|
|
4710
|
+
module.exports = VariableValidate;
|
|
4711
|
+
},{"./RuleTemplate.ebnf":15,"./TemplateFilters":18,"@halleyassist/rule-parser":2,"ebnf":13}]},{},[14])(14)
|
|
4240
4712
|
});
|
package/index.d.ts
CHANGED
|
@@ -14,8 +14,9 @@ export interface VariableValue {
|
|
|
14
14
|
from: string;
|
|
15
15
|
to: string;
|
|
16
16
|
ago?: [number, string];
|
|
17
|
-
};
|
|
18
|
-
|
|
17
|
+
} | Record<string, any> | string[] | number[] | boolean[] | Record<string, any>[];
|
|
18
|
+
filters?: string[];
|
|
19
|
+
type?: 'string' | 'number' | 'boolean' | 'object' | 'time period' | 'time period ago' | 'time value' | 'number time' | 'string array' | 'number array' | 'boolean array' | 'object array';
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface Variables {
|
|
@@ -27,6 +28,11 @@ export interface ValidationResult {
|
|
|
27
28
|
errors: string[];
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export interface VariableValidationResult {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export interface ASTNode {
|
|
31
37
|
type: string;
|
|
32
38
|
text?: string;
|
|
@@ -139,6 +145,14 @@ export class VariableTemplate {
|
|
|
139
145
|
format(variableData: VariableValue | Variables): VariableValue;
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
export class VariableValidate {
|
|
149
|
+
static VariableTypes: string[];
|
|
150
|
+
static validators: Record<string, (value: any) => VariableValidationResult>;
|
|
151
|
+
static validate(variableData: VariableValue): VariableValidationResult;
|
|
152
|
+
static validateValue(type: string, value: any): VariableValidationResult;
|
|
153
|
+
static isValid(type: string, value: any): boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
export const ParserRules: any[];
|
|
143
157
|
export const VariableTypes: string[];
|
|
144
158
|
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');
|
|
@@ -247,6 +248,14 @@ class RuleTemplate {
|
|
|
247
248
|
// Validate type if provided
|
|
248
249
|
if (type && !VariableTypes.includes(type)) {
|
|
249
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
|
+
}
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -367,6 +376,11 @@ class RuleTemplate {
|
|
|
367
376
|
}
|
|
368
377
|
}
|
|
369
378
|
|
|
379
|
+
const validation = VariableValidate.validate(varData);
|
|
380
|
+
if (!validation.valid) {
|
|
381
|
+
throw new Error(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
370
384
|
return this._serializeVarData(varData, varName);
|
|
371
385
|
}
|
|
372
386
|
|
|
@@ -397,6 +411,10 @@ class RuleTemplate {
|
|
|
397
411
|
return ret;
|
|
398
412
|
}
|
|
399
413
|
|
|
414
|
+
if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
|
|
415
|
+
return JSON.stringify(value);
|
|
416
|
+
}
|
|
417
|
+
|
|
400
418
|
return String(value);
|
|
401
419
|
}
|
|
402
420
|
|
|
@@ -423,5 +441,6 @@ class RuleTemplate {
|
|
|
423
441
|
RuleTemplate.ParserRules = ParserRules;
|
|
424
442
|
RuleTemplate.VariableTypes = VariableTypes;
|
|
425
443
|
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
444
|
+
RuleTemplate.VariableValidate = VariableValidate;
|
|
426
445
|
|
|
427
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');
|
|
@@ -247,6 +248,14 @@ class RuleTemplate {
|
|
|
247
248
|
// Validate type if provided
|
|
248
249
|
if (type && !VariableTypes.includes(type)) {
|
|
249
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
|
+
}
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -367,6 +376,11 @@ class RuleTemplate {
|
|
|
367
376
|
}
|
|
368
377
|
}
|
|
369
378
|
|
|
379
|
+
const validation = VariableValidate.validate(varData);
|
|
380
|
+
if (!validation.valid) {
|
|
381
|
+
throw new Error(`Invalid value for variable '${varName}': ${validation.error}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
370
384
|
return this._serializeVarData(varData, varName);
|
|
371
385
|
}
|
|
372
386
|
|
|
@@ -397,6 +411,10 @@ class RuleTemplate {
|
|
|
397
411
|
return ret;
|
|
398
412
|
}
|
|
399
413
|
|
|
414
|
+
if (type === 'object' || type === 'string array' || type === 'number array' || type === 'boolean array' || type === 'object array') {
|
|
415
|
+
return JSON.stringify(value);
|
|
416
|
+
}
|
|
417
|
+
|
|
400
418
|
return String(value);
|
|
401
419
|
}
|
|
402
420
|
|
|
@@ -423,5 +441,6 @@ class RuleTemplate {
|
|
|
423
441
|
RuleTemplate.ParserRules = ParserRules;
|
|
424
442
|
RuleTemplate.VariableTypes = VariableTypes;
|
|
425
443
|
RuleTemplate.TemplateFilters = TemplateFilters;
|
|
444
|
+
RuleTemplate.VariableValidate = VariableValidate;
|
|
426
445
|
|
|
427
446
|
module.exports = RuleTemplate;
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
const { Parser } = require('ebnf');
|
|
2
|
+
const TemplateGrammar = require('./RuleTemplate.ebnf');
|
|
3
|
+
const TemplateFilters = require('./TemplateFilters');
|
|
4
|
+
const RuleParser = require('@halleyassist/rule-parser');
|
|
5
|
+
const RuleParserRules = RuleParser.ParserRules;
|
|
6
|
+
|
|
7
|
+
const VariableTypes = [
|
|
8
|
+
'string',
|
|
9
|
+
'number',
|
|
10
|
+
'boolean',
|
|
11
|
+
'object',
|
|
12
|
+
'time period',
|
|
13
|
+
'time period ago',
|
|
14
|
+
'time value',
|
|
15
|
+
'number time',
|
|
16
|
+
'string array',
|
|
17
|
+
'number array',
|
|
18
|
+
'boolean array',
|
|
19
|
+
'object array'
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
let ParserCache = null;
|
|
23
|
+
|
|
24
|
+
const ValidationRules = [...RuleParserRules];
|
|
25
|
+
for (const rule of TemplateGrammar) {
|
|
26
|
+
const idx = ValidationRules.findIndex(existingRule => existingRule.name === rule.name);
|
|
27
|
+
if (idx !== -1) {
|
|
28
|
+
ValidationRules[idx] = rule;
|
|
29
|
+
} else {
|
|
30
|
+
ValidationRules.push(rule);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class VariableValidate {
|
|
35
|
+
static validate(variableData) {
|
|
36
|
+
if (!variableData || typeof variableData !== 'object' || Array.isArray(variableData)) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
error: 'Variable data must be an object with value and type properties'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!Object.prototype.hasOwnProperty.call(variableData, 'type')) {
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
error: 'Variable data must include a type property'
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let normalizedVarData;
|
|
51
|
+
try {
|
|
52
|
+
normalizedVarData = VariableValidate._normalizeVarData(variableData);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
error: error.message
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return VariableValidate.validateValue(normalizedVarData.type, normalizedVarData.value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static validateValue(type, value) {
|
|
64
|
+
const validator = VariableValidate.validators[type];
|
|
65
|
+
if (!validator) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
error: `Unsupported variable type '${type}'`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return validator(value);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
valid: false,
|
|
77
|
+
error: error.message
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static isValid(type, value) {
|
|
83
|
+
return VariableValidate.validateValue(type, value).valid;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
static _validateWithRule(value, startRule, options = {}) {
|
|
87
|
+
const parser = VariableValidate._getParser();
|
|
88
|
+
const normalized = options.normalize ? options.normalize(value) : value;
|
|
89
|
+
|
|
90
|
+
if (typeof normalized !== 'string' || !normalized.length) {
|
|
91
|
+
return {
|
|
92
|
+
valid: false,
|
|
93
|
+
error: options.emptyMessage || `Expected ${startRule} input`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
parser.getAST(normalized, startRule);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
error: options.parseMessage || error.message
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (options.semanticCheck) {
|
|
107
|
+
const semanticError = options.semanticCheck(value, normalized);
|
|
108
|
+
if (semanticError) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: semanticError
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { valid: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static _getParser() {
|
|
120
|
+
if (!ParserCache) {
|
|
121
|
+
ParserCache = new Parser(ValidationRules, { debug: false });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return ParserCache;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
static _normalizeVarData(variableData) {
|
|
128
|
+
const normalizedVarData = VariableValidate._cloneVarData(variableData);
|
|
129
|
+
|
|
130
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedVarData, 'value')) {
|
|
131
|
+
throw new Error('Variable data must include a value property');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!Object.prototype.hasOwnProperty.call(normalizedVarData, 'filters')) {
|
|
135
|
+
return normalizedVarData;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!Array.isArray(normalizedVarData.filters)) {
|
|
139
|
+
throw new Error('Variable data filters must be an array');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const filterName of normalizedVarData.filters) {
|
|
143
|
+
if (!TemplateFilters[filterName]) {
|
|
144
|
+
throw new Error(`Unknown filter '${filterName}'`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
TemplateFilters[filterName](normalizedVarData);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return normalizedVarData;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
static _cloneVarData(variableData) {
|
|
154
|
+
const cloned = Object.assign({}, variableData);
|
|
155
|
+
if (Array.isArray(cloned.filters)) {
|
|
156
|
+
cloned.filters = cloned.filters.slice();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (cloned.value && typeof cloned.value === 'object') {
|
|
160
|
+
if (Array.isArray(cloned.value)) {
|
|
161
|
+
cloned.value = cloned.value.slice();
|
|
162
|
+
} else {
|
|
163
|
+
cloned.value = Object.assign({}, cloned.value);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return cloned;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
static _serializeString(value) {
|
|
171
|
+
if (typeof value !== 'string') {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return JSON.stringify(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static _serializeNumber(value) {
|
|
179
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return String(value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static _serializeBoolean(value) {
|
|
187
|
+
if (typeof value !== 'boolean') {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return value ? 'true' : 'false';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static _serializeTimeValue(value) {
|
|
195
|
+
if (typeof value !== 'string') {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return value.trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static _serializeTimePeriod(value) {
|
|
203
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return `${value.from.trim()} TO ${value.to.trim()}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static _serializeTimePeriodAgo(value) {
|
|
211
|
+
if (!VariableValidate._isPlainObject(value) || typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!Array.isArray(value.ago) || value.ago.length !== 2) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const [amount, unit] = value.ago;
|
|
220
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || typeof unit !== 'string') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `${value.from.trim()} TO ${value.to.trim()} AGO ${amount} ${unit.trim()}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
static _serializeNumberTime(value) {
|
|
228
|
+
if (typeof value !== 'string') {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return value.trim();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static _serializeJsonObject(value) {
|
|
236
|
+
if (!VariableValidate._isJsonObject(value)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return JSON.stringify(value);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
static _serializeTypedArray(value, predicate) {
|
|
244
|
+
if (!Array.isArray(value) || !value.every(predicate)) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return JSON.stringify(value);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static _serializeObjectArray(value) {
|
|
252
|
+
if (!Array.isArray(value) || !value.every(item => VariableValidate._isJsonObject(item))) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return JSON.stringify(value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static _isPlainObject(value) {
|
|
260
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static _isJsonObject(value) {
|
|
264
|
+
if (!VariableValidate._isPlainObject(value)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static _isJsonValue(value) {
|
|
272
|
+
if (value === null) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (typeof value === 'string' || typeof value === 'boolean') {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (typeof value === 'number') {
|
|
281
|
+
return Number.isFinite(value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (Array.isArray(value)) {
|
|
285
|
+
return value.every(entry => VariableValidate._isJsonValue(entry));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (VariableValidate._isPlainObject(value)) {
|
|
289
|
+
return Object.values(value).every(entry => VariableValidate._isJsonValue(entry));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
static _validateTimeOfDay(value) {
|
|
296
|
+
if (typeof value !== 'string') {
|
|
297
|
+
return 'Time value must be a string in HH:MM format';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const match = value.trim().match(/^(\d{1,2}):(\d{1,2})$/);
|
|
301
|
+
if (!match) {
|
|
302
|
+
return 'Time value must be a string in HH:MM format';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const hours = Number(match[1]);
|
|
306
|
+
const minutes = Number(match[2]);
|
|
307
|
+
|
|
308
|
+
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
309
|
+
return 'Time value must contain hours 0-23 and minutes 0-59';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
VariableValidate.VariableTypes = VariableTypes.slice();
|
|
317
|
+
VariableValidate.validators = Object.freeze({
|
|
318
|
+
'string': (value) => VariableValidate._validateWithRule(value, 'string_atom', {
|
|
319
|
+
normalize: VariableValidate._serializeString,
|
|
320
|
+
emptyMessage: 'String variables must be JavaScript strings',
|
|
321
|
+
parseMessage: 'String variables must serialize to a valid quoted string literal'
|
|
322
|
+
}),
|
|
323
|
+
'number': (value) => VariableValidate._validateWithRule(value, 'number', {
|
|
324
|
+
normalize: VariableValidate._serializeNumber,
|
|
325
|
+
emptyMessage: 'Number variables must be finite numbers',
|
|
326
|
+
parseMessage: 'Number variables must serialize to a valid numeric literal'
|
|
327
|
+
}),
|
|
328
|
+
'boolean': (value) => VariableValidate._validateWithRule(value, 'boolean_atom', {
|
|
329
|
+
normalize: VariableValidate._serializeBoolean,
|
|
330
|
+
emptyMessage: 'Boolean variables must be JavaScript booleans',
|
|
331
|
+
parseMessage: 'Boolean variables must serialize to a valid boolean literal'
|
|
332
|
+
}),
|
|
333
|
+
'object': (value) => VariableValidate._validateWithRule(value, 'object_atom', {
|
|
334
|
+
normalize: VariableValidate._serializeJsonObject,
|
|
335
|
+
emptyMessage: 'Object variables must be plain JSON-compatible objects',
|
|
336
|
+
parseMessage: 'Object variables must serialize to valid JSON object syntax'
|
|
337
|
+
}),
|
|
338
|
+
'time period': (value) => VariableValidate._validateWithRule(value, 'time_period_atom', {
|
|
339
|
+
normalize: VariableValidate._serializeTimePeriod,
|
|
340
|
+
emptyMessage: 'Time period variables must be objects with string from/to properties',
|
|
341
|
+
parseMessage: 'Time period variables must serialize to FROM TO TO syntax',
|
|
342
|
+
semanticCheck: (rawValue) => {
|
|
343
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
344
|
+
if (fromError) return `Invalid time period from value: ${fromError}`;
|
|
345
|
+
|
|
346
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
347
|
+
if (toError) return `Invalid time period to value: ${toError}`;
|
|
348
|
+
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
}),
|
|
352
|
+
'time period ago': (value) => VariableValidate._validateWithRule(value, 'time_period_ago_atom', {
|
|
353
|
+
normalize: VariableValidate._serializeTimePeriodAgo,
|
|
354
|
+
emptyMessage: 'Time period ago variables must be objects with from, to, and ago properties',
|
|
355
|
+
parseMessage: 'Time period ago variables must serialize to FROM TO TO AGO AMOUNT UNIT syntax',
|
|
356
|
+
semanticCheck: (rawValue) => {
|
|
357
|
+
const fromError = VariableValidate._validateTimeOfDay(rawValue?.from);
|
|
358
|
+
if (fromError) return `Invalid time period ago from value: ${fromError}`;
|
|
359
|
+
|
|
360
|
+
const toError = VariableValidate._validateTimeOfDay(rawValue?.to);
|
|
361
|
+
if (toError) return `Invalid time period ago to value: ${toError}`;
|
|
362
|
+
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}),
|
|
366
|
+
'time value': (value) => VariableValidate._validateWithRule(value, 'time_value_atom', {
|
|
367
|
+
normalize: VariableValidate._serializeTimeValue,
|
|
368
|
+
emptyMessage: 'Time value variables must be strings in HH:MM format',
|
|
369
|
+
parseMessage: 'Time value variables must serialize to HH:MM syntax',
|
|
370
|
+
semanticCheck: (rawValue) => VariableValidate._validateTimeOfDay(rawValue)
|
|
371
|
+
}),
|
|
372
|
+
'number time': (value) => VariableValidate._validateWithRule(value, 'number_time', {
|
|
373
|
+
normalize: VariableValidate._serializeNumberTime,
|
|
374
|
+
emptyMessage: 'Number time variables must be strings like "2 hours"',
|
|
375
|
+
parseMessage: 'Number time variables must serialize to NUMBER UNIT syntax'
|
|
376
|
+
}),
|
|
377
|
+
'string array': (value) => VariableValidate._validateWithRule(value, 'string_array', {
|
|
378
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'string'),
|
|
379
|
+
emptyMessage: 'String array variables must be arrays of strings',
|
|
380
|
+
parseMessage: 'String array variables must serialize to a valid string array literal'
|
|
381
|
+
}),
|
|
382
|
+
'number array': (value) => VariableValidate._validateWithRule(value, 'number_array', {
|
|
383
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'number' && Number.isFinite(item)),
|
|
384
|
+
emptyMessage: 'Number array variables must be arrays of finite numbers',
|
|
385
|
+
parseMessage: 'Number array variables must serialize to a valid number array literal'
|
|
386
|
+
}),
|
|
387
|
+
'boolean array': (value) => VariableValidate._validateWithRule(value, 'boolean_array', {
|
|
388
|
+
normalize: (rawValue) => VariableValidate._serializeTypedArray(rawValue, item => typeof item === 'boolean'),
|
|
389
|
+
emptyMessage: 'Boolean array variables must be arrays of booleans',
|
|
390
|
+
parseMessage: 'Boolean array variables must serialize to a valid boolean array literal'
|
|
391
|
+
}),
|
|
392
|
+
'object array': (value) => VariableValidate._validateWithRule(value, 'object_array', {
|
|
393
|
+
normalize: VariableValidate._serializeObjectArray,
|
|
394
|
+
emptyMessage: 'Object array variables must be arrays of plain JSON-compatible objects',
|
|
395
|
+
parseMessage: 'Object array variables must serialize to a valid object array literal'
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
module.exports = VariableValidate;
|