@halleyassist/rule-templater 0.0.1

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 ADDED
@@ -0,0 +1,231 @@
1
+ # rule-templater
2
+
3
+ Parsing and preparation of rule templates for HalleyAssist rules.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @halleyassist/rule-templater
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ The `rule-templater` package provides utilities for working with rule templates that contain variable placeholders.
14
+
15
+ ### Basic Example
16
+
17
+ ```javascript
18
+ const RuleTemplate = require('@halleyassist/rule-templater');
19
+
20
+ // Define a template with variables
21
+ const template = 'EventIs(${EVENT_TYPE}) && Value() > ${THRESHOLD}';
22
+
23
+ // Parse the template to get a RuleTemplate instance
24
+ const parsed = RuleTemplate.parse(template);
25
+
26
+ // Extract variables from the template (uses AST)
27
+ const variables = parsed.extractVariables();
28
+ console.log(variables);
29
+ // [
30
+ // { name: 'EVENT_TYPE', filters: [] },
31
+ // { name: 'THRESHOLD', filters: [] }
32
+ // ]
33
+
34
+ // Validate that variables are provided correctly
35
+ const validation = parsed.validate({
36
+ EVENT_TYPE: { value: 'sensor-update', type: 'string' },
37
+ THRESHOLD: { value: 42, type: 'number' }
38
+ });
39
+ console.log(validation.valid); // true
40
+
41
+ // Prepare the template with actual values
42
+ const prepared = parsed.prepare({
43
+ EVENT_TYPE: { value: 'sensor-update', type: 'string' },
44
+ THRESHOLD: { value: 42, type: 'number' }
45
+ });
46
+ console.log(prepared);
47
+ // 'EventIs("sensor-update") && Value() > 42'
48
+ ```
49
+
50
+ ### Complex Example
51
+
52
+ ```javascript
53
+ const template = '!(EventIs(StrConcat("DeviceEvent:measurement:", ${ACTION})) && TimeLastTrueSet("last_measurement") || TimeLastTrueCheck("last_measurement") < ${TIME})';
54
+
55
+ const parsed = RuleTemplate.parse(template);
56
+
57
+ // Extract variables
58
+ const variables = parsed.extractVariables();
59
+ // [
60
+ // { name: 'ACTION', filters: [] },
61
+ // { name: 'TIME', filters: [] }
62
+ // ]
63
+
64
+ // Prepare with values
65
+ const prepared = parsed.prepare({
66
+ ACTION: { value: 'temperature', type: 'string' },
67
+ TIME: { value: 60, type: 'number' }
68
+ });
69
+
70
+ // Result: !(EventIs(StrConcat("DeviceEvent:measurement:", "temperature")) && TimeLastTrueSet("last_measurement") || TimeLastTrueCheck("last_measurement") < 60)
71
+ ```
72
+
73
+ ### Template Filters
74
+
75
+ Variables can have filters applied to transform their values. Filters are applied using the pipe (`|`) syntax:
76
+
77
+ ```javascript
78
+ const template = 'EventIs(${EVENT_TYPE|upper})';
79
+
80
+ const parsed = RuleTemplate.parse(template);
81
+ const variables = parsed.extractVariables();
82
+ // [{ name: 'EVENT_TYPE', filters: ['upper'] }]
83
+
84
+ // Prepare with filters applied
85
+ const prepared = parsed.prepare({
86
+ EVENT_TYPE: { value: 'sensor-update' }
87
+ });
88
+ // Result: EventIs(SENSOR-UPDATE)
89
+ ```
90
+
91
+ #### Multiple Filters
92
+
93
+ Filters can be chained together and are applied in sequence:
94
+
95
+ ```javascript
96
+ const template = 'EventIs(${EVENT|trim|upper|string})';
97
+
98
+ const parsed = RuleTemplate.parse(template);
99
+ const prepared = parsed.prepare({
100
+ EVENT: { value: ' test ' }
101
+ });
102
+ // Result: EventIs("TEST")
103
+ ```
104
+
105
+ #### Available Filters
106
+
107
+ - **string**: Convert to JSON string representation (adds quotes and escapes)
108
+ - **upper**: Convert to uppercase
109
+ - **lower**: Convert to lowercase
110
+ - **capitalize**: Capitalize first letter
111
+ - **title**: Convert to title case (capitalize each word)
112
+ - **trim**: Remove leading/trailing whitespace
113
+ - **number**: Convert to number
114
+ - **boolean**: Convert to boolean
115
+ - **abs**: Absolute value (for numbers)
116
+ - **round**: Round number to nearest integer
117
+ - **floor**: Round number down
118
+ - **ceil**: Round number up
119
+
120
+ #### Filter Examples
121
+
122
+ ```javascript
123
+ // String transformation
124
+ '${name|upper}' with name='john' → JOHN
125
+ '${name|capitalize}' with name='john doe' → John doe
126
+ '${name|title}' with name='john doe' → John Doe
127
+
128
+ // Number operations
129
+ '${value|abs}' with value=-42 → 42
130
+ '${value|round}' with value=3.7 → 4
131
+ '${value|floor}' with value=3.9 → 3
132
+
133
+ // Chaining filters
134
+ '${text|trim|upper}' with text=' hello ' → HELLO
135
+ ```
136
+
137
+ ## API
138
+
139
+ ### `RuleTemplate.parse(ruleTemplate)`
140
+
141
+ Parses a rule template string and returns a RuleTemplate instance.
142
+
143
+ **Parameters:**
144
+ - `ruleTemplate` (string): The template string containing `${VARIABLE}` placeholders
145
+
146
+ **Returns:** A `RuleTemplate` instance with:
147
+ - `ruleTemplateText`: The original template string
148
+ - `ast`: The parsed Abstract Syntax Tree
149
+
150
+ ### `ruleTemplate.extractVariables()`
151
+
152
+ Extracts all variables from the template using the AST.
153
+
154
+ **Returns:** Array of objects with:
155
+ - `name` (string): The variable name
156
+ - `filters` (array): Array of filter names applied to the variable
157
+
158
+ ### `ruleTemplate.validate(variables)`
159
+
160
+ Validates that all required variables are provided and have valid types.
161
+
162
+ **Parameters:**
163
+ - `variables` (object): Object mapping variable names to their values and types
164
+ - Each variable should be an object with:
165
+ - `value`: The value to substitute (string, number, or boolean)
166
+ - `type` (optional): The variable type ('string', 'number', 'boolean', etc.)
167
+
168
+ **Returns:** Object with:
169
+ - `valid` (boolean): Whether validation passed
170
+ - `errors` (array): Array of error messages (empty if valid)
171
+
172
+ ### `ruleTemplate.prepare(variables)`
173
+
174
+ Prepares the template by replacing variables with their values and applying any filters.
175
+
176
+ **Parameters:**
177
+ - `variables` (object): Object mapping variable names to their values and types
178
+ - Each variable should be an object with:
179
+ - `value`: The value to substitute (string, number, or boolean)
180
+ - `type` (optional): The variable type ('string', 'number', 'boolean', etc.)
181
+
182
+ **Returns:** The prepared rule string with variables replaced and filters applied
183
+
184
+ ### `RuleTemplate.validateVariableNode(astNode, variableType)` (Static)
185
+
186
+ Helper method to validate that an AST node matches the expected variable type.
187
+
188
+ **Parameters:**
189
+ - `astNode`: The AST node to validate
190
+ - `variableType` (string): The expected variable type
191
+
192
+ **Returns:** `true` if the node is valid for the given type, `false` otherwise
193
+
194
+ ### `RuleTemplate.TemplateFilters` (Static)
195
+
196
+ Access to the filter functions used by the template engine. Can be extended with custom filters.
197
+
198
+ **Example:**
199
+ ```javascript
200
+ const RuleTemplate = require('@halleyassist/rule-templater');
201
+
202
+ // Add a custom filter
203
+ RuleTemplate.TemplateFilters.reverse = (value) => {
204
+ return String(value).split('').reverse().join('');
205
+ };
206
+
207
+ // Use the custom filter
208
+ const template = 'EventIs(${EVENT|reverse})';
209
+ const parsed = RuleTemplate.parse(template);
210
+ const result = parsed.prepare({ EVENT: { value: 'test' } });
211
+ // Result: EventIs(tset)
212
+ ```
213
+
214
+ ## Variable Types
215
+
216
+ The following variable types are supported:
217
+
218
+ - `string`
219
+ - `number`
220
+ - `boolean`
221
+ - `object`
222
+ - `time period`
223
+ - `time value`
224
+ - `string array`
225
+ - `number array`
226
+ - `boolean array`
227
+ - `object array`
228
+
229
+ ## License
230
+
231
+ ISC
package/index.d.ts ADDED
@@ -0,0 +1,91 @@
1
+ export interface VariableInfo {
2
+ name: string;
3
+ filters: string[];
4
+ }
5
+
6
+ export interface VariableValue {
7
+ value: string | number | boolean;
8
+ type?: 'string' | 'number' | 'boolean' | 'object' | 'time period' | 'time value' | 'string array' | 'number array' | 'boolean array' | 'object array';
9
+ }
10
+
11
+ export interface Variables {
12
+ [key: string]: VariableValue;
13
+ }
14
+
15
+ export interface ValidationResult {
16
+ valid: boolean;
17
+ errors: string[];
18
+ }
19
+
20
+ export interface ASTNode {
21
+ type: string;
22
+ text?: string;
23
+ children?: ASTNode[];
24
+ [key: string]: any;
25
+ }
26
+
27
+ export type FilterFunction = (value: any) => any;
28
+
29
+ export interface TemplateFiltersType {
30
+ string: FilterFunction;
31
+ upper: FilterFunction;
32
+ lower: FilterFunction;
33
+ capitalize: FilterFunction;
34
+ title: FilterFunction;
35
+ trim: FilterFunction;
36
+ number: FilterFunction;
37
+ boolean: FilterFunction;
38
+ abs: FilterFunction;
39
+ round: FilterFunction;
40
+ floor: FilterFunction;
41
+ ceil: FilterFunction;
42
+ default: FilterFunction;
43
+ [key: string]: FilterFunction;
44
+ }
45
+
46
+ export default class RuleTemplate {
47
+ ruleTemplateText: string;
48
+ ast: ASTNode;
49
+
50
+ constructor(ruleTemplateText: string, ast: ASTNode);
51
+
52
+ /**
53
+ * Parse a rule template string and return a RuleTemplate instance
54
+ * @param ruleTemplate The template string to parse
55
+ * @returns Instance with AST and template text
56
+ */
57
+ static parse(ruleTemplate: string): RuleTemplate;
58
+
59
+ /**
60
+ * Extract variables from the template using the AST
61
+ * @returns Array of {name, filters} objects
62
+ */
63
+ extractVariables(): VariableInfo[];
64
+
65
+ /**
66
+ * Validate variable types against the AST
67
+ * @param variables Object mapping variable names to {value, type} objects
68
+ * @returns Object with validation results: {valid, errors}
69
+ */
70
+ validate(variables: Variables): ValidationResult;
71
+
72
+ /**
73
+ * Prepare the template by replacing variables with their values
74
+ * Applies any filters specified in the template (e.g., ${var|upper|trim})
75
+ * @param variables Object mapping variable names to {value, type} objects
76
+ * @returns The prepared rule string
77
+ */
78
+ prepare(variables: Variables): string;
79
+
80
+ /**
81
+ * Helper method to validate if an AST node matches a variable type
82
+ * @param astNode The AST node to validate
83
+ * @param variableType The expected variable type
84
+ * @returns True if valid, false otherwise
85
+ */
86
+ static validateVariableNode(astNode: ASTNode | null | undefined, variableType: string): boolean;
87
+ }
88
+
89
+ export const ParserRules: any[];
90
+ export const VariableTypes: string[];
91
+ export const TemplateFilters: TemplateFiltersType;
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./src/RuleTemplater')
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@halleyassist/rule-templater",
3
+ "version": "0.0.1",
4
+ "description": "The grammar for HalleyAssist rules",
5
+ "main": "index.js",
6
+ "browser": "./dist/rule-templater.browser.js",
7
+ "types": "index.d.ts",
8
+ "scripts": {
9
+ "test": "mocha",
10
+ "build": "gulp build"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/HalleyAssist/rule-templater.git"
15
+ },
16
+ "author": "",
17
+ "license": "ISC",
18
+ "bugs": {
19
+ "url": "https://github.com/HalleyAssist/rule-templater/issues"
20
+ },
21
+ "homepage": "https://github.com/HalleyAssist/rule-templater#readme",
22
+ "dependencies": {
23
+ "@halleyassist/rule-parser": "^1.0.19",
24
+ "ebnf": "git+https://github.com/HalleyAssist/node-ebnf.git"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^25.1.0",
28
+ "browserify": "^17.0.1",
29
+ "chai": "^4",
30
+ "gulp": "^5.0.1",
31
+ "mocha": "^10.8.2",
32
+ "typescript": "^5.9.3",
33
+ "unassertify": "^3.0.1",
34
+ "vinyl-buffer": "^1.0.1",
35
+ "vinyl-source-stream": "^2.0.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "files": [
42
+ "src/*",
43
+ "index.js",
44
+ "index.d.ts",
45
+ "dist/*"
46
+ ]
47
+ }
@@ -0,0 +1,27 @@
1
+ const {Grammars} = require('ebnf');
2
+
3
+ const grammar = `
4
+ TEMPLATE_BEGIN ::= "\${"
5
+ TEMPLATE_END ::= "}"
6
+ PIPE ::= "|"
7
+ IDENT ::= [A-Za-z_][A-Za-z0-9_]*
8
+ DOT ::= "."
9
+
10
+ template_value ::= TEMPLATE_BEGIN WS* template_expr WS* TEMPLATE_END
11
+
12
+ template_expr ::= template_path (WS* template_pipe WS* template_filter_call)*
13
+
14
+ template_pipe ::= PIPE
15
+
16
+ template_path ::= IDENT (WS* DOT WS* IDENT)*
17
+
18
+ template_filter_call ::= template_filter_name (WS* BEGIN_ARGUMENT WS* template_filter_args? WS* END_ARGUMENT)?
19
+
20
+ template_filter_name ::= IDENT
21
+
22
+ template_filter_args ::= template_filter_arg (WS* "," WS* template_filter_arg)*
23
+
24
+ template_filter_arg ::= value | template_value
25
+ `
26
+
27
+ module.exports = Grammars.W3C.getRules(grammar);
@@ -0,0 +1 @@
1
+ module.exports = require('./RuleTemplater.production.js');
@@ -0,0 +1,340 @@
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;
340
+ module.exports.TemplateFilters = TemplateFilters;
@@ -0,0 +1,63 @@
1
+ /*
2
+ Template filters are functions that transform variable values.
3
+ They are applied in the template syntax as ${variable|filter} or ${variable|filter1|filter2}
4
+ */
5
+ const TemplateFilters = {
6
+ // Convert value to JSON string representation
7
+ string: value => JSON.stringify(String(value)),
8
+
9
+ // Convert to uppercase
10
+ upper: value => String(value).toUpperCase(),
11
+
12
+ // Convert to lowercase
13
+ lower: value => String(value).toLowerCase(),
14
+
15
+ // Capitalize first letter
16
+ capitalize: value => {
17
+ const str = String(value);
18
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
19
+ },
20
+
21
+ // Convert to title case
22
+ title: value => {
23
+ return String(value).split(' ')
24
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
25
+ .join(' ');
26
+ },
27
+
28
+ // Trim whitespace
29
+ trim: value => String(value).trim(),
30
+
31
+ // Convert to number
32
+ number: value => Number(value),
33
+
34
+ // Convert to boolean
35
+ boolean: value => {
36
+ if (typeof value === 'boolean') return value;
37
+ if (typeof value === 'string') {
38
+ const lower = value.toLowerCase();
39
+ if (lower === 'true' || lower === '1' || lower === 'yes') return true;
40
+ if (lower === 'false' || lower === '0' || lower === 'no') return false;
41
+ }
42
+ return Boolean(value);
43
+ },
44
+
45
+ // Convert to absolute value (for numbers)
46
+ abs: value => Math.abs(Number(value)),
47
+
48
+ // Round number
49
+ round: value => Math.round(Number(value)),
50
+
51
+ // Floor number
52
+ floor: value => Math.floor(Number(value)),
53
+
54
+ // Ceil number
55
+ ceil: value => Math.ceil(Number(value)),
56
+
57
+ // Default value if empty/null/undefined
58
+ default: (value, defaultValue = '') => {
59
+ return (value === null || value === undefined || value === '') ? defaultValue : value;
60
+ }
61
+ }
62
+
63
+ module.exports = TemplateFilters;