@halleyassist/rule-templater 0.0.1 → 0.0.4

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.
@@ -1,340 +1,349 @@
1
- // Note: We import the internal RuleParser.ebnf to extend the grammar with template rules.
2
- // This creates coupling to the internal structure of @halleyassist/rule-parser.
3
- const RuleParserRules = require('@halleyassist/rule-parser/src/RuleParser.ebnf'),
4
- TemplateGrammar = require('./RuleTemplate.ebnf'),
5
- TemplateFilters = require('./TemplateFilters'),
6
- {Parser} = require('ebnf');
7
-
8
- let ParserCache = null;
9
-
10
- const VariableTypes = [
11
- 'string',
12
- 'number',
13
- 'boolean',
14
- 'object',
15
- 'time period',
16
- 'time value',
17
- 'string array',
18
- 'number array',
19
- 'boolean array',
20
- 'object array'
21
- ]
22
-
23
- const AllowedTypeMapping = {
24
- 'string': ['string_atom', 'string_concat'],
25
- 'number': ['number_atom', 'math_expr'],
26
- 'boolean': ['boolean_atom', 'boolean_expr'],
27
- 'time period': ['time_period_atom'],
28
- 'time value': ['time_value_atom', 'tod_atom'],
29
- 'string array': ['string_array'],
30
- 'number array': ['number_array'],
31
- 'boolean array': ['boolean_array'],
32
- 'object': ['object_atom'],
33
- 'object array': ['object_array']
34
- };
35
-
36
- // Merge the base grammar with template-specific grammar rules
37
- const extendedGrammar = [...RuleParserRules]
38
- for(const rule of TemplateGrammar){
39
- const idx = extendedGrammar.findIndex(r => r.name === rule.name);
40
- if(idx !== -1){
41
- extendedGrammar[idx] = rule;
42
- } else {
43
- extendedGrammar.push(rule);
44
- }
45
- }
46
-
47
- // Add template_value as an alternative to value_atom so templates can be parsed
48
- const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
49
- if (valueAtomIdx !== -1) {
50
- extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
51
- }
52
-
53
- // Export the parser rules for potential external use
54
- const ParserRules = extendedGrammar;
55
-
56
- class RuleTemplate {
57
- constructor(ruleTemplateText, ast) {
58
- this.ruleTemplateText = ruleTemplateText;
59
- this.ast = ast;
60
- }
61
-
62
- /**
63
- * Parse a rule template string and return a RuleTemplate instance
64
- * @param {string} ruleTemplate - The template string to parse
65
- * @returns {RuleTemplate} Instance with AST and template text
66
- */
67
- static parse(ruleTemplate){
68
- if(!ParserCache){
69
- ParserCache = new Parser(ParserRules, {debug: false})
70
- }
71
-
72
- const ast = ParserCache.getAST(ruleTemplate.trim(), 'statement_main');
73
- return new RuleTemplate(ruleTemplate, ast);
74
- }
75
-
76
- /**
77
- * Extract variables from the template using the AST
78
- * @returns {Array} Array of {name, filters: []} objects
79
- */
80
- extractVariables(){
81
- const variables = [];
82
- const seen = new Set();
83
-
84
- const traverse = (node) => {
85
- if (!node) return;
86
-
87
- // Check if this is a template_value node
88
- if (node.type === 'template_value') {
89
- // Extract the variable information
90
- const varInfo = this._extractVariableFromNode(node);
91
- if (varInfo && !seen.has(varInfo.name)) {
92
- seen.add(varInfo.name);
93
- variables.push(varInfo);
94
- }
95
- }
96
-
97
- // Traverse children
98
- if (node.children) {
99
- for (const child of node.children) {
100
- traverse(child);
101
- }
102
- }
103
- };
104
-
105
- traverse(this.ast);
106
- return variables;
107
- }
108
-
109
- /**
110
- * Extract variable name and filters from a template_value AST node
111
- * @private
112
- */
113
- _extractVariableFromNode(node) {
114
- if (node.type !== 'template_value') return null;
115
-
116
- // Find the template_expr child
117
- const templateExpr = node.children?.find(c => c.type === 'template_expr');
118
- if (!templateExpr) return null;
119
-
120
- // Extract the path (variable name)
121
- const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
122
- if (!templatePath || !templatePath.text) return null;
123
-
124
- const name = templatePath.text.trim();
125
-
126
- // Extract filters
127
- const filters = [];
128
- for (const child of templateExpr.children || []) {
129
- if (child.type === 'template_filter_call') {
130
- const filterName = this._extractFilterName(child);
131
- if (filterName) {
132
- filters.push(filterName);
133
- }
134
- }
135
- }
136
-
137
- return { name, filters };
138
- }
139
-
140
- /**
141
- * Extract filter name from template_filter_call node
142
- * @private
143
- */
144
- _extractFilterName(node) {
145
- const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
146
- if (!filterNameNode || !filterNameNode.text) return null;
147
-
148
- return filterNameNode.text.trim();
149
- }
150
-
151
- /**
152
- * Validate variable types against the AST
153
- * @param {Object} variables - Object mapping variable names to {type} objects
154
- * @returns {Object} Object with validation results: {valid: boolean, errors: []}
155
- */
156
- validate(variables) {
157
- if (!variables || typeof variables !== 'object') {
158
- return {
159
- valid: false,
160
- errors: ['Variables must be provided as an object']
161
- };
162
- }
163
-
164
- const errors = [];
165
- const extractedVars = this.extractVariables();
166
-
167
- for (const varInfo of extractedVars) {
168
- const varName = varInfo.name;
169
-
170
- // Check if variable is provided
171
- if (!variables.hasOwnProperty(varName)) {
172
- errors.push(`Variable '${varName}' not provided in variables object`);
173
- continue;
174
- }
175
-
176
- const varData = variables[varName];
177
- if (typeof varData !== 'object') {
178
- errors.push(`Variable '${varName}' must be an object`);
179
- continue;
180
- }
181
-
182
- const { type } = varData;
183
-
184
- // Validate type if provided
185
- if (type && !VariableTypes.includes(type)) {
186
- errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
187
- }
188
- }
189
-
190
- return {
191
- valid: errors.length === 0,
192
- errors
193
- };
194
- }
195
-
196
- /**
197
- * Prepare the template by replacing variables with their values
198
- * Rebuilds from AST by iterating through children
199
- * @param {Object} variables - Object mapping variable names to {value, type} objects
200
- * @returns {string} The prepared rule string
201
- */
202
- prepare(variables){
203
- if (!variables || typeof variables !== 'object') {
204
- throw new Error('Variables must be provided as an object');
205
- }
206
-
207
- // Rebuild the rule string from AST
208
- return this._rebuildFromAST(this.ast, variables);
209
- }
210
-
211
- /**
212
- * Rebuild rule string from AST node, replacing template_value nodes with variable values
213
- * @private
214
- * @param {Object} node - AST node
215
- * @param {Object} variables - Object mapping variable names to {value, type} objects
216
- * @returns {string} Rebuilt string
217
- */
218
- _rebuildFromAST(node, variables) {
219
- if (!node) return '';
220
-
221
- // If this is a template_value node, replace it with the computed value
222
- if (node.type === 'template_value') {
223
- return this._computeTemplateReplacement(node, variables);
224
- }
225
-
226
- // If node has no children, it's a leaf - return its text
227
- if (!node.children || node.children.length === 0) {
228
- return node.text || '';
229
- }
230
-
231
- // Node has children - rebuild by iterating through children and preserving gaps
232
- let result = '';
233
- const originalText = node.text || '';
234
- let lastEnd = node.start || 0;
235
-
236
- for (const child of node.children) {
237
- // Add any text between the last child's end and this child's start (gaps/syntax)
238
- if (child.start !== undefined && child.start > lastEnd) {
239
- result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
240
- }
241
-
242
- // Add the child's rebuilt text
243
- result += this._rebuildFromAST(child, variables);
244
-
245
- // Update lastEnd to this child's end position
246
- if (child.end !== undefined) {
247
- lastEnd = child.end;
248
- }
249
- }
250
-
251
- // Add any remaining text after the last child
252
- if (node.end !== undefined && lastEnd < node.end) {
253
- result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
254
- }
255
-
256
- return result;
257
- }
258
-
259
- /**
260
- * Compute the replacement value for a template_value node
261
- * @private
262
- * @param {Object} node - template_value AST node
263
- * @param {Object} variables - Object mapping variable names to {value, type} objects
264
- * @returns {string} Replacement string
265
- */
266
- _computeTemplateReplacement(node, variables) {
267
- const templateInfo = this._extractVariableFromNode(node);
268
- if (!templateInfo) {
269
- throw new Error(`Failed to extract variable information from template node`);
270
- }
271
-
272
- const varName = templateInfo.name;
273
-
274
- // Validate variable is provided
275
- if (!variables.hasOwnProperty(varName)) {
276
- throw new Error(`Variable '${varName}' not provided in variables object`);
277
- }
278
-
279
- const varData = variables[varName];
280
- if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
281
- throw new Error(`Variable '${varName}' must be an object with 'value' property`);
282
- }
283
-
284
- let { value, type } = varData;
285
-
286
- // Validate type if provided
287
- if (type && !VariableTypes.includes(type)) {
288
- throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
289
- }
290
-
291
- // Apply filters if present
292
- if (templateInfo.filters && templateInfo.filters.length > 0) {
293
- for (const filterName of templateInfo.filters) {
294
- if (!TemplateFilters[filterName]) {
295
- throw new Error(`Unknown filter '${filterName}'`);
296
- }
297
- value = TemplateFilters[filterName](value);
298
- }
299
- // After applying filters, the result is already a string representation
300
- return String(value);
301
- }
302
-
303
- // Convert value to string representation based on type
304
- if (type === 'string') {
305
- return JSON.stringify(String(value));
306
- } else if (type === 'number') {
307
- return String(value);
308
- } else if (type === 'boolean') {
309
- return value ? 'TRUE' : 'FALSE';
310
- } else {
311
- // Default behavior - just insert the value as-is
312
- return String(value);
313
- }
314
- }
315
-
316
- /**
317
- * Helper method to validate if an AST node matches a variable type
318
- * @param {Object} astNode - The AST node to validate
319
- * @param {string} variableType - The expected variable type
320
- * @returns {boolean} True if valid, false otherwise
321
- */
322
- static validateVariableNode(astNode, variableType) {
323
- if (!astNode || !astNode.type) {
324
- return false;
325
- }
326
-
327
- const allowedTypes = AllowedTypeMapping[variableType];
328
- if (!allowedTypes) {
329
- return false;
330
- }
331
-
332
- return allowedTypes.includes(astNode.type);
333
- }
334
- }
335
-
336
- // Export the class and parser rules
337
- module.exports = RuleTemplate;
338
- module.exports.ParserRules = ParserRules;
339
- module.exports.VariableTypes = VariableTypes;
1
+ // Note: We import the internal RuleParser.ebnf to extend the grammar with template rules.
2
+ // This creates coupling to the internal structure of @halleyassist/rule-parser.
3
+ const RuleParserRules = require('@halleyassist/rule-parser/src/RuleParser.ebnf'),
4
+ TemplateGrammar = require('./RuleTemplate.ebnf'),
5
+ TemplateFilters = require('./TemplateFilters'),
6
+ {Parser} = require('ebnf');
7
+
8
+ let ParserCache = null;
9
+
10
+ const VariableTypes = [
11
+ 'string',
12
+ 'number',
13
+ 'boolean',
14
+ 'object',
15
+ 'time period',
16
+ 'time value',
17
+ 'string array',
18
+ 'number array',
19
+ 'boolean array',
20
+ 'object array'
21
+ ]
22
+
23
+ const AllowedTypeMapping = {
24
+ 'string': ['string_atom', 'string_concat'],
25
+ 'number': ['number_atom', 'math_expr'],
26
+ 'boolean': ['boolean_atom', 'boolean_expr'],
27
+ 'time period': ['time_period_atom'],
28
+ 'time value': ['time_value_atom', 'tod_atom'],
29
+ 'string array': ['string_array'],
30
+ 'number array': ['number_array'],
31
+ 'boolean array': ['boolean_array'],
32
+ 'object': ['object_atom'],
33
+ 'object array': ['object_array']
34
+ };
35
+
36
+ // Merge the base grammar with template-specific grammar rules
37
+ const extendedGrammar = [...RuleParserRules]
38
+ for(const rule of TemplateGrammar){
39
+ const idx = extendedGrammar.findIndex(r => r.name === rule.name);
40
+ if(idx !== -1){
41
+ extendedGrammar[idx] = rule;
42
+ } else {
43
+ extendedGrammar.push(rule);
44
+ }
45
+ }
46
+
47
+ // Add template_value as an alternative to value_atom so templates can be parsed
48
+ const valueAtomIdx = extendedGrammar.findIndex(r => r.name === 'value_atom');
49
+ if (valueAtomIdx !== -1) {
50
+ extendedGrammar[valueAtomIdx].bnf.push(['template_value']);
51
+ }
52
+
53
+ // Export the parser rules for potential external use
54
+ const ParserRules = extendedGrammar;
55
+
56
+ class RuleTemplate {
57
+ constructor(ruleTemplateText, ast) {
58
+ this.ruleTemplateText = ruleTemplateText;
59
+ this.ast = ast;
60
+ }
61
+
62
+ /**
63
+ * Parse a rule template string and return a RuleTemplate instance
64
+ * @param {string} ruleTemplate - The template string to parse
65
+ * @returns {RuleTemplate} Instance with AST and template text
66
+ */
67
+ static parse(ruleTemplate){
68
+ if(!ParserCache){
69
+ ParserCache = new Parser(ParserRules, {debug: false})
70
+ }
71
+
72
+ const ast = ParserCache.getAST(ruleTemplate.trim(), 'statement_main');
73
+ return new RuleTemplate(ruleTemplate, ast);
74
+ }
75
+
76
+ /**
77
+ * Extract variables from the template using the AST
78
+ * @returns {Array} Array of {name, filters: []} objects
79
+ */
80
+ extractVariables(){
81
+ const variables = [];
82
+ const seen = new Set();
83
+
84
+ const traverse = (node) => {
85
+ if (!node) return;
86
+
87
+ // Check if this is a template_value node
88
+ if (node.type === 'template_value') {
89
+ // Extract the variable information
90
+ const varInfo = this._extractVariableFromNode(node);
91
+ if (varInfo && !seen.has(varInfo.name)) {
92
+ seen.add(varInfo.name);
93
+ variables.push(varInfo);
94
+ }
95
+ }
96
+
97
+ // Traverse children
98
+ if (node.children) {
99
+ for (const child of node.children) {
100
+ traverse(child);
101
+ }
102
+ }
103
+ };
104
+
105
+ traverse(this.ast);
106
+ return variables;
107
+ }
108
+
109
+ /**
110
+ * Extract variable name and filters from a template_value AST node
111
+ * @private
112
+ */
113
+ _extractVariableFromNode(node) {
114
+ if (node.type !== 'template_value') return null;
115
+
116
+ // Find the template_expr child
117
+ const templateExpr = node.children?.find(c => c.type === 'template_expr');
118
+ if (!templateExpr) return null;
119
+
120
+ // Extract the path (variable name)
121
+ const templatePath = templateExpr.children?.find(c => c.type === 'template_path');
122
+ if (!templatePath || !templatePath.text) return null;
123
+
124
+ const name = templatePath.text.trim();
125
+
126
+ // Extract filters
127
+ const filters = [];
128
+ for (const child of templateExpr.children || []) {
129
+ if (child.type === 'template_filter_call') {
130
+ const filterName = this._extractFilterName(child);
131
+ if (filterName) {
132
+ filters.push(filterName);
133
+ }
134
+ }
135
+ }
136
+
137
+ return { name, filters };
138
+ }
139
+
140
+ /**
141
+ * Extract filter name from template_filter_call node
142
+ * @private
143
+ */
144
+ _extractFilterName(node) {
145
+ const filterNameNode = node.children?.find(c => c.type === 'template_filter_name');
146
+ if (!filterNameNode || !filterNameNode.text) return null;
147
+
148
+ return filterNameNode.text.trim();
149
+ }
150
+
151
+ /**
152
+ * Validate variable types against the AST
153
+ * @param {Object} variables - Object mapping variable names to {type} objects
154
+ * @returns {Object} Object with validation results: {valid: boolean, errors: []}
155
+ */
156
+ validate(variables) {
157
+ if (!variables || typeof variables !== 'object') {
158
+ return {
159
+ valid: false,
160
+ errors: ['Variables must be provided as an object']
161
+ };
162
+ }
163
+
164
+ const errors = [];
165
+ const extractedVars = this.extractVariables();
166
+
167
+ for (const varInfo of extractedVars) {
168
+ const varName = varInfo.name;
169
+
170
+ // Check if variable is provided
171
+ if (!variables.hasOwnProperty(varName)) {
172
+ errors.push(`Variable '${varName}' not provided in variables object`);
173
+ continue;
174
+ }
175
+
176
+ const varData = variables[varName];
177
+ if (typeof varData !== 'object') {
178
+ errors.push(`Variable '${varName}' must be an object`);
179
+ continue;
180
+ }
181
+
182
+ const { type } = varData;
183
+
184
+ // Validate type if provided
185
+ if (type && !VariableTypes.includes(type)) {
186
+ errors.push(`Invalid variable type '${type}' for variable '${varName}'`);
187
+ }
188
+ }
189
+
190
+ return {
191
+ valid: errors.length === 0,
192
+ errors
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Prepare the template by replacing variables with their values
198
+ * Rebuilds from AST by iterating through children
199
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
200
+ * @returns {string} The prepared rule string
201
+ */
202
+ prepare(variables){
203
+ if (!variables || typeof variables !== 'object') {
204
+ throw new Error('Variables must be provided as an object');
205
+ }
206
+
207
+ // Rebuild the rule string from AST
208
+ return this._rebuildFromAST(this.ast, variables);
209
+ }
210
+
211
+ /**
212
+ * Rebuild rule string from AST node, replacing template_value nodes with variable values
213
+ * @private
214
+ * @param {Object} node - AST node
215
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
216
+ * @returns {string} Rebuilt string
217
+ */
218
+ _rebuildFromAST(node, variables) {
219
+ if (!node) return '';
220
+
221
+ // If this is a template_value node, replace it with the computed value
222
+ if (node.type === 'template_value') {
223
+ return this._computeTemplateReplacement(node, variables);
224
+ }
225
+
226
+ // If node has no children, it's a leaf - return its text
227
+ if (!node.children || node.children.length === 0) {
228
+ return node.text || '';
229
+ }
230
+
231
+ // Node has children - rebuild by iterating through children and preserving gaps
232
+ let result = '';
233
+ const originalText = node.text || '';
234
+ let lastEnd = node.start || 0;
235
+
236
+ for (const child of node.children) {
237
+ // Add any text between the last child's end and this child's start (gaps/syntax)
238
+ if (child.start !== undefined && child.start > lastEnd) {
239
+ result += originalText.substring(lastEnd - (node.start || 0), child.start - (node.start || 0));
240
+ }
241
+
242
+ // Add the child's rebuilt text
243
+ result += this._rebuildFromAST(child, variables);
244
+
245
+ // Update lastEnd to this child's end position
246
+ if (child.end !== undefined) {
247
+ lastEnd = child.end;
248
+ }
249
+ }
250
+
251
+ // Add any remaining text after the last child
252
+ if (node.end !== undefined && lastEnd < node.end) {
253
+ result += originalText.substring(lastEnd - (node.start || 0), node.end - (node.start || 0));
254
+ }
255
+
256
+ return result;
257
+ }
258
+
259
+ /**
260
+ * Compute the replacement value for a template_value node
261
+ * @private
262
+ * @param {Object} node - template_value AST node
263
+ * @param {Object} variables - Object mapping variable names to {value, type} objects
264
+ * @returns {string} Replacement string
265
+ */
266
+ _computeTemplateReplacement(node, variables) {
267
+ const templateInfo = this._extractVariableFromNode(node);
268
+ if (!templateInfo) {
269
+ throw new Error(`Failed to extract variable information from template node`);
270
+ }
271
+
272
+ const varName = templateInfo.name;
273
+
274
+ // Validate variable is provided
275
+ if (!variables.hasOwnProperty(varName)) {
276
+ throw new Error(`Variable '${varName}' not provided in variables object`);
277
+ }
278
+
279
+ const varData = variables[varName];
280
+ if (typeof varData !== 'object' || !varData.hasOwnProperty('value')) {
281
+ throw new Error(`Variable '${varName}' must be an object with 'value' property`);
282
+ }
283
+
284
+ let { value, type } = varData;
285
+
286
+ // Require type property for all variables
287
+ if (!varData.hasOwnProperty('type')) {
288
+ throw new Error(`Variable '${varName}' must have a 'type' property`);
289
+ }
290
+
291
+ // Validate type
292
+ if (!VariableTypes.includes(type)) {
293
+ throw new Error(`Invalid variable type '${type}' for variable '${varName}'`);
294
+ }
295
+
296
+ // Apply filters if present
297
+ if (templateInfo.filters && templateInfo.filters.length > 0) {
298
+ for (const filterName of templateInfo.filters) {
299
+ if (!TemplateFilters[filterName]) {
300
+ throw new Error(`Unknown filter '${filterName}'`);
301
+ }
302
+ value = TemplateFilters[filterName](value);
303
+ }
304
+ // After applying filters, the result is already a string representation
305
+ return String(value);
306
+ }
307
+
308
+ // Convert value to string representation based on type
309
+ if (type === 'string') {
310
+ // Escape backslashes first, then quotes in string values.
311
+ // Order is critical: escaping backslashes first prevents double-escaping.
312
+ // E.g., "test\" becomes "test\\" then "test\\\"" (correct)
313
+ // If reversed, "test\" would become "test\\"" then "test\\\\"" (incorrect)
314
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
315
+ } else if (type === 'number') {
316
+ return String(value);
317
+ } else if (type === 'boolean') {
318
+ return value ? 'true' : 'false';
319
+ } else {
320
+ // Default behavior - just insert the value as-is
321
+ return String(value);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Helper method to validate if an AST node matches a variable type
327
+ * @param {Object} astNode - The AST node to validate
328
+ * @param {string} variableType - The expected variable type
329
+ * @returns {boolean} True if valid, false otherwise
330
+ */
331
+ static validateVariableNode(astNode, variableType) {
332
+ if (!astNode || !astNode.type) {
333
+ return false;
334
+ }
335
+
336
+ const allowedTypes = AllowedTypeMapping[variableType];
337
+ if (!allowedTypes) {
338
+ return false;
339
+ }
340
+
341
+ return allowedTypes.includes(astNode.type);
342
+ }
343
+ }
344
+
345
+ // Export the class and parser rules
346
+ module.exports = RuleTemplate;
347
+ module.exports.ParserRules = ParserRules;
348
+ module.exports.VariableTypes = VariableTypes;
340
349
  module.exports.TemplateFilters = TemplateFilters;