@halleyassist/rule-templater 0.0.16 → 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
package/index.d.ts CHANGED
@@ -26,6 +26,7 @@ export interface Variables {
26
26
  export interface ValidationResult {
27
27
  valid: boolean;
28
28
  errors: string[];
29
+ warnings: string[];
29
30
  }
30
31
 
31
32
  export interface VariableValidationResult {
@@ -60,6 +61,17 @@ export interface TemplateFiltersType {
60
61
  [key: string]: FilterFunction;
61
62
  }
62
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
+
63
75
  export class RuleTemplate {
64
76
  ruleTemplateText: string;
65
77
  ast: ASTNode;
@@ -90,7 +102,7 @@ export class RuleTemplate {
90
102
  * @param variables Object mapping variable names to {value, type} objects
91
103
  * @returns Object with validation results: {valid, errors}
92
104
  */
93
- validate(variables: Variables): ValidationResult;
105
+ validate(variables: Variables, functionBlob?: HalleyFunctionBlob): ValidationResult;
94
106
 
95
107
  /**
96
108
  * Prepare the template by replacing variables with their values
@@ -125,6 +137,18 @@ export class GeneralTemplate {
125
137
  prepare(variables: Variables): string;
126
138
  }
127
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
+
128
152
  export class VariableTemplate {
129
153
  templateText: string;
130
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.16",
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