@halleyassist/rule-templater 0.0.14 → 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 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":16}],15:[function(require,module,exports){
3649
- module.exports=[{"name":"TEMPLATE_BEGIN","bnf":[["\"${\""]]},{"name":"TEMPLATE_END","bnf":[["\"}\""]]},{"name":"PIPE","bnf":[["\"|\""]]},{"name":"%IDENT[2]","bnf":[[/[A-Za-z0-9_]/]]},{"name":"IDENT","bnf":[[/[A-Za-z_]/,"%IDENT[2]*"]]},{"name":"DOT","bnf":[["\".\""]]},{"name":"template_value","bnf":[["TEMPLATE_BEGIN","WS*","template_expr","WS*","TEMPLATE_END"]]},{"name":"%template_expr[2]","bnf":[["WS*","template_pipe","WS*","template_filter_call"]],"fragment":true},{"name":"template_expr","bnf":[["template_path","%template_expr[2]*"]]},{"name":"template_pipe","bnf":[["PIPE"]]},{"name":"%template_path[2]","bnf":[["WS*","DOT","WS*","IDENT"]],"fragment":true},{"name":"template_path","bnf":[["IDENT","%template_path[2]*"]]},{"name":"%template_filter_call[2]","bnf":[["WS*","BEGIN_ARGUMENT","WS*","template_filter_args?","WS*","END_ARGUMENT"]],"fragment":true},{"name":"template_filter_call","bnf":[["template_filter_name","%template_filter_call[2]?"]]},{"name":"template_filter_name","bnf":[["IDENT"]]},{"name":"%template_filter_args[2]","bnf":[["WS*","\",\"","WS*","template_filter_arg"]],"fragment":true},{"name":"template_filter_args","bnf":[["template_filter_arg","%template_filter_args[2]*"]]},{"name":"template_filter_arg","bnf":[["value"],["template_value"]]},{"name":"number_atom","bnf":[["number"],["template_value"]]},{"name":"number_time_atom","bnf":[["number_time"],["template_value","WS+","unit"],["template_value"]]},{"name":"tod_atom","bnf":[["number_tod"],["template_value"]]},{"name":"dow_atom","bnf":[["dow"],["template_value"]]},{"name":"between_time_only_atom","bnf":[["between_time_only"],["template_value"]]},{"name":"between_tod_only_atom","bnf":[["between_tod_only"],["template_value"]]}]
3650
- },{}],16:[function(require,module,exports){
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":15,"./TemplateFilters":17,"@halleyassist/rule-parser":2,"ebnf":13}],17:[function(require,module,exports){
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,351 @@ const TemplateFilters = {
4236
4308
  }
4237
4309
 
4238
4310
  module.exports = TemplateFilters;
4239
- },{}]},{},[14])(14)
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)
4240
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-templater",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleTemplate.production.js",
6
6
  "browser": "./dist/rule-templater.browser.js",
@@ -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);
@@ -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,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;