@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 +231 -0
- package/index.d.ts +91 -0
- package/index.js +1 -0
- package/package.json +47 -0
- package/src/RuleTemplate.ebnf.js +27 -0
- package/src/RuleTemplater.browser.js +1 -0
- package/src/RuleTemplater.js +340 -0
- package/src/TemplateFilters.js +63 -0
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;
|