@halleyassist/rule-templater 0.0.15 → 0.0.17

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.
@@ -3918,17 +3918,20 @@ class RuleTemplate {
3918
3918
  /**
3919
3919
  * Validate variable types against the AST
3920
3920
  * @param {Object} variables - Object mapping variable names to {type} objects
3921
- * @returns {Object} Object with validation results: {valid: boolean, errors: []}
3921
+ * @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
3922
+ * @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
3922
3923
  */
3923
- validate(variables) {
3924
+ validate(variables, functionBlob) {
3924
3925
  if (!variables || typeof variables !== 'object') {
3925
3926
  return {
3926
3927
  valid: false,
3927
- errors: ['Variables must be provided as an object']
3928
+ errors: ['Variables must be provided as an object'],
3929
+ warnings: []
3928
3930
  };
3929
3931
  }
3930
3932
 
3931
3933
  const errors = [];
3934
+ const warnings = [];
3932
3935
  const extractedVars = this.extractVariables();
3933
3936
 
3934
3937
  for (const varInfo of extractedVars) {
@@ -3961,13 +3964,50 @@ class RuleTemplate {
3961
3964
  }
3962
3965
  }
3963
3966
  }
3967
+
3968
+ if (functionBlob && typeof functionBlob.validate === 'function') {
3969
+ for (const functionCall of this._extractFunctionCalls()) {
3970
+ warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
3971
+ }
3972
+ }
3964
3973
 
3965
3974
  return {
3966
3975
  valid: errors.length === 0,
3967
- errors
3976
+ errors,
3977
+ warnings
3968
3978
  };
3969
3979
  }
3970
3980
 
3981
+ _extractFunctionCalls() {
3982
+ const functionCalls = [];
3983
+
3984
+ const traverse = (node) => {
3985
+ if (!node) return;
3986
+
3987
+ if (node.type === 'fcall') {
3988
+ const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
3989
+ const argumentsNode = node.children?.find(c => c.type === 'arguments');
3990
+ if (functionName) {
3991
+ functionCalls.push({
3992
+ name: functionName,
3993
+ arguments: argumentsNode?.children
3994
+ ?.filter(c => c.type === 'argument')
3995
+ .map(c => c.text) || []
3996
+ });
3997
+ }
3998
+ }
3999
+
4000
+ if (node.children) {
4001
+ for (const child of node.children) {
4002
+ traverse(child);
4003
+ }
4004
+ }
4005
+ };
4006
+
4007
+ traverse(this.ast);
4008
+ return functionCalls;
4009
+ }
4010
+
3971
4011
  /**
3972
4012
  * Prepare the template by replacing variables with their values
3973
4013
  * Rebuilds from AST by iterating through children
@@ -4311,6 +4351,7 @@ module.exports = TemplateFilters;
4311
4351
  },{}],19:[function(require,module,exports){
4312
4352
  const { Parser } = require('ebnf');
4313
4353
  const TemplateGrammar = require('./RuleTemplate.ebnf');
4354
+ const TemplateFilters = require('./TemplateFilters');
4314
4355
  const RuleParser = require('@halleyassist/rule-parser');
4315
4356
  const RuleParserRules = RuleParser.ParserRules;
4316
4357
 
@@ -4357,7 +4398,17 @@ class VariableValidate {
4357
4398
  };
4358
4399
  }
4359
4400
 
4360
- return VariableValidate.validateValue(variableData.type, variableData.value);
4401
+ let normalizedVarData;
4402
+ try {
4403
+ normalizedVarData = VariableValidate._normalizeVarData(variableData);
4404
+ } catch (error) {
4405
+ return {
4406
+ valid: false,
4407
+ error: error.message
4408
+ };
4409
+ }
4410
+
4411
+ return VariableValidate.validateValue(normalizedVarData.type, normalizedVarData.value);
4361
4412
  }
4362
4413
 
4363
4414
  static validateValue(type, value) {
@@ -4424,6 +4475,49 @@ class VariableValidate {
4424
4475
  return ParserCache;
4425
4476
  }
4426
4477
 
4478
+ static _normalizeVarData(variableData) {
4479
+ const normalizedVarData = VariableValidate._cloneVarData(variableData);
4480
+
4481
+ if (!Object.prototype.hasOwnProperty.call(normalizedVarData, 'value')) {
4482
+ throw new Error('Variable data must include a value property');
4483
+ }
4484
+
4485
+ if (!Object.prototype.hasOwnProperty.call(normalizedVarData, 'filters')) {
4486
+ return normalizedVarData;
4487
+ }
4488
+
4489
+ if (!Array.isArray(normalizedVarData.filters)) {
4490
+ throw new Error('Variable data filters must be an array');
4491
+ }
4492
+
4493
+ for (const filterName of normalizedVarData.filters) {
4494
+ if (!TemplateFilters[filterName]) {
4495
+ throw new Error(`Unknown filter '${filterName}'`);
4496
+ }
4497
+
4498
+ TemplateFilters[filterName](normalizedVarData);
4499
+ }
4500
+
4501
+ return normalizedVarData;
4502
+ }
4503
+
4504
+ static _cloneVarData(variableData) {
4505
+ const cloned = Object.assign({}, variableData);
4506
+ if (Array.isArray(cloned.filters)) {
4507
+ cloned.filters = cloned.filters.slice();
4508
+ }
4509
+
4510
+ if (cloned.value && typeof cloned.value === 'object') {
4511
+ if (Array.isArray(cloned.value)) {
4512
+ cloned.value = cloned.value.slice();
4513
+ } else {
4514
+ cloned.value = Object.assign({}, cloned.value);
4515
+ }
4516
+ }
4517
+
4518
+ return cloned;
4519
+ }
4520
+
4427
4521
  static _serializeString(value) {
4428
4522
  if (typeof value !== 'string') {
4429
4523
  return null;
@@ -4654,5 +4748,5 @@ VariableValidate.validators = Object.freeze({
4654
4748
  });
4655
4749
 
4656
4750
  module.exports = VariableValidate;
4657
- },{"./RuleTemplate.ebnf":15,"@halleyassist/rule-parser":2,"ebnf":13}]},{},[14])(14)
4751
+ },{"./RuleTemplate.ebnf":15,"./TemplateFilters":18,"@halleyassist/rule-parser":2,"ebnf":13}]},{},[14])(14)
4658
4752
  });
package/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface VariableValue {
15
15
  to: string;
16
16
  ago?: [number, string];
17
17
  } | Record<string, any> | string[] | number[] | boolean[] | Record<string, any>[];
18
+ filters?: string[];
18
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
 
@@ -25,6 +26,7 @@ export interface Variables {
25
26
  export interface ValidationResult {
26
27
  valid: boolean;
27
28
  errors: string[];
29
+ warnings: string[];
28
30
  }
29
31
 
30
32
  export interface VariableValidationResult {
@@ -59,6 +61,17 @@ export interface TemplateFiltersType {
59
61
  [key: string]: FilterFunction;
60
62
  }
61
63
 
64
+ export interface HalleyFunctionDefinition {
65
+ name: string;
66
+ arguments: string[];
67
+ }
68
+
69
+ export interface HalleyFunctionBlobData {
70
+ _schema?: number;
71
+ version?: string;
72
+ functions?: HalleyFunctionDefinition[];
73
+ }
74
+
62
75
  export class RuleTemplate {
63
76
  ruleTemplateText: string;
64
77
  ast: ASTNode;
@@ -89,7 +102,7 @@ export class RuleTemplate {
89
102
  * @param variables Object mapping variable names to {value, type} objects
90
103
  * @returns Object with validation results: {valid, errors}
91
104
  */
92
- validate(variables: Variables): ValidationResult;
105
+ validate(variables: Variables, functionBlob?: HalleyFunctionBlob): ValidationResult;
93
106
 
94
107
  /**
95
108
  * Prepare the template by replacing variables with their values
@@ -124,6 +137,18 @@ export class GeneralTemplate {
124
137
  prepare(variables: Variables): string;
125
138
  }
126
139
 
140
+ export class HalleyFunctionBlob {
141
+ _schema?: number;
142
+ version?: string;
143
+ functions: HalleyFunctionDefinition[];
144
+
145
+ constructor(jsonData: HalleyFunctionBlobData);
146
+
147
+ static fromURL(url: string): Promise<HalleyFunctionBlob>;
148
+
149
+ validate(functionName: string, variables?: any[]): string[];
150
+ }
151
+
127
152
  export class VariableTemplate {
128
153
  templateText: string;
129
154
  ast: ASTNode;
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const RuleTemplate = require('./src/RuleTemplate');
2
2
  const GeneralTemplate = require('./src/GeneralTemplate');
3
+ const HalleyFunctionBlob = require('./src/HalleyFunctionBlob');
3
4
  const VariableTemplate = require('./src/VariableTemplate');
4
5
  const VariableValidate = require('./src/VariableValidate');
5
6
 
@@ -9,4 +10,5 @@ module.exports.VariableTypes = RuleTemplate.VariableTypes;
9
10
  module.exports.TemplateFilters = RuleTemplate.TemplateFilters;
10
11
  module.exports.VariableValidate = VariableValidate;
11
12
  module.exports.GeneralTemplate = GeneralTemplate;
13
+ module.exports.HalleyFunctionBlob = HalleyFunctionBlob;
12
14
  module.exports.VariableTemplate = VariableTemplate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halleyassist/rule-templater",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "The grammar for HalleyAssist rules",
5
5
  "main": "src/RuleTemplate.production.js",
6
6
  "browser": "./dist/rule-templater.browser.js",
@@ -0,0 +1,126 @@
1
+ class HalleyFunctionBlob {
2
+ constructor(jsonData) {
3
+ const blobData = jsonData && typeof jsonData === 'object' ? jsonData : {};
4
+
5
+ this._schema = blobData._schema;
6
+ this.version = blobData.version;
7
+ this.functions = [];
8
+ this.functionMap = new Map();
9
+
10
+ const functions = Array.isArray(blobData.functions) ? blobData.functions : [];
11
+ for (const definition of functions) {
12
+ if (!definition || typeof definition.name !== 'string') {
13
+ continue;
14
+ }
15
+
16
+ const normalizedDefinition = {
17
+ name: definition.name,
18
+ arguments: Array.isArray(definition.arguments) ? definition.arguments.slice() : []
19
+ };
20
+
21
+ this.functions.push(normalizedDefinition);
22
+ this.functionMap.set(normalizedDefinition.name, normalizedDefinition);
23
+ }
24
+ }
25
+
26
+ static async fromURL(url) {
27
+ if (typeof url !== 'string' || !url.trim()) {
28
+ throw new Error('A function blob URL must be provided');
29
+ }
30
+
31
+ if (typeof globalThis.fetch !== 'function') {
32
+ throw new Error('Fetch API is not available');
33
+ }
34
+
35
+ const response = await globalThis.fetch(url);
36
+ if (!response || !response.ok) {
37
+ throw new Error(`Failed to fetch function blob from '${url}'`);
38
+ }
39
+
40
+ return new HalleyFunctionBlob(await response.json());
41
+ }
42
+
43
+ validate(functionName, variables = []) {
44
+ const warnings = [];
45
+ const functionDefinition = this.functionMap.get(functionName);
46
+
47
+ if (!functionDefinition) {
48
+ return [`function '${functionName}' does not exist`];
49
+ }
50
+
51
+ const providedVariables = Array.isArray(variables) ? variables : [];
52
+ const providedCount = providedVariables.length;
53
+ const parameterRange = this._getParameterRange(functionDefinition.arguments);
54
+
55
+ for (let idx = 0; idx < functionDefinition.arguments.length; idx++) {
56
+ const argumentName = functionDefinition.arguments[idx];
57
+ if (argumentName === '...') {
58
+ break;
59
+ }
60
+
61
+ if (providedCount > idx || argumentName.endsWith('?')) {
62
+ continue;
63
+ }
64
+
65
+ warnings.push(
66
+ `parameter ${idx + 1} of ${functionName} '${argumentName}' is missing, function expects ${parameterRange}`
67
+ );
68
+ }
69
+
70
+ if (this._hasTooManyArguments(functionDefinition.arguments, providedCount)) {
71
+ warnings.push(
72
+ `${functionName} received ${providedCount} parameters, function expects ${parameterRange}`
73
+ );
74
+ }
75
+
76
+ return warnings;
77
+ }
78
+
79
+ _hasTooManyArguments(argumentList, providedCount) {
80
+ const { max } = this._getArgumentBounds(argumentList);
81
+ return max !== Infinity && providedCount > max;
82
+ }
83
+
84
+ _getParameterRange(argumentList) {
85
+ const { min, max } = this._getArgumentBounds(argumentList);
86
+
87
+ if (max === Infinity) {
88
+ if (min === 0) {
89
+ return 'any number of parameters';
90
+ }
91
+
92
+ return `at least ${min} ${min === 1 ? 'parameter' : 'parameters'}`;
93
+ }
94
+
95
+ if (min === max) {
96
+ return `${min} ${min === 1 ? 'parameter' : 'parameters'}`;
97
+ }
98
+
99
+ return `${min} to ${max} parameters`;
100
+ }
101
+
102
+ _getArgumentBounds(argumentList) {
103
+ let min = 0;
104
+ let max = 0;
105
+ let variadic = false;
106
+
107
+ for (const argumentName of argumentList) {
108
+ if (argumentName === '...') {
109
+ variadic = true;
110
+ continue;
111
+ }
112
+
113
+ max++;
114
+ if (!argumentName.endsWith('?')) {
115
+ min++;
116
+ }
117
+ }
118
+
119
+ return {
120
+ min,
121
+ max: variadic ? Infinity : max
122
+ };
123
+ }
124
+ }
125
+
126
+ module.exports = HalleyFunctionBlob;
@@ -215,17 +215,20 @@ class RuleTemplate {
215
215
  /**
216
216
  * Validate variable types against the AST
217
217
  * @param {Object} variables - Object mapping variable names to {type} objects
218
- * @returns {Object} Object with validation results: {valid: boolean, errors: []}
218
+ * @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
219
+ * @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
219
220
  */
220
- validate(variables) {
221
+ validate(variables, functionBlob) {
221
222
  if (!variables || typeof variables !== 'object') {
222
223
  return {
223
224
  valid: false,
224
- errors: ['Variables must be provided as an object']
225
+ errors: ['Variables must be provided as an object'],
226
+ warnings: []
225
227
  };
226
228
  }
227
229
 
228
230
  const errors = [];
231
+ const warnings = [];
229
232
  const extractedVars = this.extractVariables();
230
233
 
231
234
  for (const varInfo of extractedVars) {
@@ -258,13 +261,50 @@ class RuleTemplate {
258
261
  }
259
262
  }
260
263
  }
264
+
265
+ if (functionBlob && typeof functionBlob.validate === 'function') {
266
+ for (const functionCall of this._extractFunctionCalls()) {
267
+ warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
268
+ }
269
+ }
261
270
 
262
271
  return {
263
272
  valid: errors.length === 0,
264
- errors
273
+ errors,
274
+ warnings
265
275
  };
266
276
  }
267
277
 
278
+ _extractFunctionCalls() {
279
+ const functionCalls = [];
280
+
281
+ const traverse = (node) => {
282
+ if (!node) return;
283
+
284
+ if (node.type === 'fcall') {
285
+ const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
286
+ const argumentsNode = node.children?.find(c => c.type === 'arguments');
287
+ if (functionName) {
288
+ functionCalls.push({
289
+ name: functionName,
290
+ arguments: argumentsNode?.children
291
+ ?.filter(c => c.type === 'argument')
292
+ .map(c => c.text) || []
293
+ });
294
+ }
295
+ }
296
+
297
+ if (node.children) {
298
+ for (const child of node.children) {
299
+ traverse(child);
300
+ }
301
+ }
302
+ };
303
+
304
+ traverse(this.ast);
305
+ return functionCalls;
306
+ }
307
+
268
308
  /**
269
309
  * Prepare the template by replacing variables with their values
270
310
  * Rebuilds from AST by iterating through children
@@ -215,17 +215,20 @@ class RuleTemplate {
215
215
  /**
216
216
  * Validate variable types against the AST
217
217
  * @param {Object} variables - Object mapping variable names to {type} objects
218
- * @returns {Object} Object with validation results: {valid: boolean, errors: []}
218
+ * @param {Object} [functionBlob] - Optional HalleyFunctionBlob used for non-fatal function warnings
219
+ * @returns {Object} Object with validation results: {valid: boolean, errors: [], warnings: []}
219
220
  */
220
- validate(variables) {
221
+ validate(variables, functionBlob) {
221
222
  if (!variables || typeof variables !== 'object') {
222
223
  return {
223
224
  valid: false,
224
- errors: ['Variables must be provided as an object']
225
+ errors: ['Variables must be provided as an object'],
226
+ warnings: []
225
227
  };
226
228
  }
227
229
 
228
230
  const errors = [];
231
+ const warnings = [];
229
232
  const extractedVars = this.extractVariables();
230
233
 
231
234
  for (const varInfo of extractedVars) {
@@ -258,13 +261,50 @@ class RuleTemplate {
258
261
  }
259
262
  }
260
263
  }
264
+
265
+ if (functionBlob && typeof functionBlob.validate === 'function') {
266
+ for (const functionCall of this._extractFunctionCalls()) {
267
+ warnings.push(...functionBlob.validate(functionCall.name, functionCall.arguments));
268
+ }
269
+ }
261
270
 
262
271
  return {
263
272
  valid: errors.length === 0,
264
- errors
273
+ errors,
274
+ warnings
265
275
  };
266
276
  }
267
277
 
278
+ _extractFunctionCalls() {
279
+ const functionCalls = [];
280
+
281
+ const traverse = (node) => {
282
+ if (!node) return;
283
+
284
+ if (node.type === 'fcall') {
285
+ const functionName = node.children?.find(c => c.type === 'fname')?.text?.trim();
286
+ const argumentsNode = node.children?.find(c => c.type === 'arguments');
287
+ if (functionName) {
288
+ functionCalls.push({
289
+ name: functionName,
290
+ arguments: argumentsNode?.children
291
+ ?.filter(c => c.type === 'argument')
292
+ .map(c => c.text) || []
293
+ });
294
+ }
295
+ }
296
+
297
+ if (node.children) {
298
+ for (const child of node.children) {
299
+ traverse(child);
300
+ }
301
+ }
302
+ };
303
+
304
+ traverse(this.ast);
305
+ return functionCalls;
306
+ }
307
+
268
308
  /**
269
309
  * Prepare the template by replacing variables with their values
270
310
  * Rebuilds from AST by iterating through children
@@ -1,5 +1,6 @@
1
1
  const { Parser } = require('ebnf');
2
2
  const TemplateGrammar = require('./RuleTemplate.ebnf');
3
+ const TemplateFilters = require('./TemplateFilters');
3
4
  const RuleParser = require('@halleyassist/rule-parser');
4
5
  const RuleParserRules = RuleParser.ParserRules;
5
6
 
@@ -46,7 +47,17 @@ class VariableValidate {
46
47
  };
47
48
  }
48
49
 
49
- return VariableValidate.validateValue(variableData.type, variableData.value);
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);
50
61
  }
51
62
 
52
63
  static validateValue(type, value) {
@@ -113,6 +124,49 @@ class VariableValidate {
113
124
  return ParserCache;
114
125
  }
115
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
+
116
170
  static _serializeString(value) {
117
171
  if (typeof value !== 'string') {
118
172
  return null;