@halleyassist/rule-parser 1.0.13 → 1.0.15
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/index.d.ts +213 -0
- package/package.json +9 -4
- package/src/RuleParser.ebnf.js +3 -3
- package/src/RuleParser.js +81 -4
- package/src/RuleParser.production.ebnf.js +1 -1
- package/src/RuleParser.production.js +81 -4
- package/src/errors/ErrorAnalyzer.js +924 -0
- package/src/errors/RuleParseError.js +64 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// TypeScript definitions for @halleyassist/rule-parser
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Position information for errors
|
|
5
|
+
*/
|
|
6
|
+
export interface Position {
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
offset: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Rule parsing error with detailed error information
|
|
14
|
+
*/
|
|
15
|
+
export class RuleParseError extends Error {
|
|
16
|
+
name: 'RuleParseError';
|
|
17
|
+
code: string;
|
|
18
|
+
hint: string;
|
|
19
|
+
line: number;
|
|
20
|
+
column: number;
|
|
21
|
+
offset: number;
|
|
22
|
+
found: string;
|
|
23
|
+
expected: string[];
|
|
24
|
+
snippet: string;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
code: string,
|
|
28
|
+
message: string,
|
|
29
|
+
hint: string,
|
|
30
|
+
position: Position,
|
|
31
|
+
found: string,
|
|
32
|
+
expected: string[],
|
|
33
|
+
snippet: string
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
toString(): string;
|
|
37
|
+
toJSON(): {
|
|
38
|
+
code: string;
|
|
39
|
+
message: string;
|
|
40
|
+
hint: string;
|
|
41
|
+
line: number;
|
|
42
|
+
column: number;
|
|
43
|
+
offset: number;
|
|
44
|
+
found: string;
|
|
45
|
+
expected: string[];
|
|
46
|
+
snippet: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parsing error from the EBNF parser
|
|
52
|
+
*/
|
|
53
|
+
export class ParsingError extends Error {
|
|
54
|
+
name: 'ParsingError';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* AST Node from the EBNF parser
|
|
59
|
+
*/
|
|
60
|
+
export interface ASTNode {
|
|
61
|
+
type: string;
|
|
62
|
+
text: string;
|
|
63
|
+
children?: ASTNode[];
|
|
64
|
+
parent?: ASTNode;
|
|
65
|
+
[key: string]: any;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Time of day value with hours and minutes
|
|
70
|
+
*/
|
|
71
|
+
export interface TimeOfDay {
|
|
72
|
+
hours: number;
|
|
73
|
+
minutes: number;
|
|
74
|
+
tod: number;
|
|
75
|
+
dow?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Primitive value types that can appear in the IL
|
|
80
|
+
*/
|
|
81
|
+
export type PrimitiveValue = string | number | boolean | TimeOfDay;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Array value type
|
|
85
|
+
*/
|
|
86
|
+
export type ArrayValue = PrimitiveValue[];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Any value that can appear in the IL
|
|
90
|
+
*/
|
|
91
|
+
export type ILValue = PrimitiveValue | ArrayValue;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Value expression in the IL
|
|
95
|
+
*/
|
|
96
|
+
export type ValueExpression = ['Value', ILValue];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Time period constant expression
|
|
100
|
+
*/
|
|
101
|
+
export type TimePeriodConst = ['TimePeriodConst', string];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Time period constant ago expression
|
|
105
|
+
*/
|
|
106
|
+
export type TimePeriodConstAgo = ['TimePeriodConstAgo', number, string];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Time period between expression
|
|
110
|
+
*/
|
|
111
|
+
export type TimePeriodBetween =
|
|
112
|
+
| ['TimePeriodBetween', TimeOfDay | number, TimeOfDay | number]
|
|
113
|
+
| ['TimePeriodBetween', TimeOfDay | number, TimeOfDay | number, string]
|
|
114
|
+
| ['TimePeriodBetween', TimeOfDay | number, TimeOfDay | number, string, string];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Time period between ago expression
|
|
118
|
+
*/
|
|
119
|
+
export type TimePeriodBetweenAgo = ['TimePeriodBetweenAgo', number, TimeOfDay, TimeOfDay];
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Any time period expression
|
|
123
|
+
*/
|
|
124
|
+
export type TimePeriodExpression =
|
|
125
|
+
| TimePeriodConst
|
|
126
|
+
| TimePeriodConstAgo
|
|
127
|
+
| TimePeriodBetween
|
|
128
|
+
| TimePeriodBetweenAgo;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Forward declaration for ILExpression to handle recursive types
|
|
132
|
+
*/
|
|
133
|
+
export type ILExpression =
|
|
134
|
+
| ValueExpression
|
|
135
|
+
| TimePeriodExpression
|
|
136
|
+
| FunctionCall
|
|
137
|
+
| ComparisonExpression
|
|
138
|
+
| LogicalExpression
|
|
139
|
+
| ArithmeticExpression
|
|
140
|
+
| BetweenExpression
|
|
141
|
+
| NotExpression;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Function call expression
|
|
145
|
+
* Note: The first element is the function name (not an operator like 'Gt', 'And', etc.)
|
|
146
|
+
* Functions can have zero or more arguments
|
|
147
|
+
*/
|
|
148
|
+
export type FunctionCall = [string, ...ILExpression[]];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Comparison operators
|
|
152
|
+
*/
|
|
153
|
+
export type ComparisonOp = 'Gt' | 'Lt' | 'Gte' | 'Lte' | 'Eq' | 'Neq';
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Comparison expression
|
|
157
|
+
*/
|
|
158
|
+
export type ComparisonExpression = [ComparisonOp, ILExpression, ILExpression];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Logical operators
|
|
162
|
+
*/
|
|
163
|
+
export type LogicalOp = 'And' | 'Or';
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Logical expression
|
|
167
|
+
* Note: Logical operators require at least two operands
|
|
168
|
+
*/
|
|
169
|
+
export type LogicalExpression = [LogicalOp, ILExpression, ILExpression, ...ILExpression[]];
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Arithmetic operators
|
|
173
|
+
*/
|
|
174
|
+
export type ArithmeticOp = 'MathAdd' | 'MathSub' | 'MathDiv' | 'MathMul' | 'MathMod' | 'Default';
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Arithmetic expression
|
|
178
|
+
*/
|
|
179
|
+
export type ArithmeticExpression = [ArithmeticOp, ILExpression, ILExpression];
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Between expression
|
|
183
|
+
*/
|
|
184
|
+
export type BetweenExpression = ['Between', ILExpression, ILExpression, ILExpression];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Not expression
|
|
188
|
+
*/
|
|
189
|
+
export type NotExpression = ['Not', ILExpression];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Rule parser class
|
|
193
|
+
*/
|
|
194
|
+
declare class RuleParser {
|
|
195
|
+
/**
|
|
196
|
+
* Parse a rule string into an Abstract Syntax Tree (AST)
|
|
197
|
+
* @param txt - The rule string to parse
|
|
198
|
+
* @returns The AST node representing the parsed rule
|
|
199
|
+
* @throws {RuleParseError} If the rule string is invalid
|
|
200
|
+
*/
|
|
201
|
+
static toAst(txt: string): ASTNode;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse a rule string into an Intermediate Language (IL) representation
|
|
205
|
+
* @param txt - The rule string to parse
|
|
206
|
+
* @returns The IL expression representing the parsed rule
|
|
207
|
+
* @throws {RuleParseError} If the rule string is invalid
|
|
208
|
+
*/
|
|
209
|
+
static toIL(txt: string): ILExpression;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default RuleParser;
|
|
213
|
+
export { RuleParser };
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@halleyassist/rule-parser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "The grammar for HalleyAssist rules",
|
|
5
5
|
"main": "src/RuleParser.production.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"test": "mocha"
|
|
8
|
+
"test": "mocha",
|
|
9
|
+
"build": "node ./bin/package.js"
|
|
8
10
|
},
|
|
9
11
|
"repository": {
|
|
10
12
|
"type": "git",
|
|
@@ -20,8 +22,10 @@
|
|
|
20
22
|
"ebnf": "git+https://github.com/HalleyAssist/node-ebnf.git"
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.1.0",
|
|
23
26
|
"chai": "^4",
|
|
24
|
-
"mocha": "^10.8.2"
|
|
27
|
+
"mocha": "^10.8.2",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
25
29
|
},
|
|
26
30
|
"publishConfig": {
|
|
27
31
|
"access": "public",
|
|
@@ -29,6 +33,7 @@
|
|
|
29
33
|
},
|
|
30
34
|
"files": [
|
|
31
35
|
"src/*",
|
|
32
|
-
"index.js"
|
|
36
|
+
"index.js",
|
|
37
|
+
"index.d.ts"
|
|
33
38
|
]
|
|
34
39
|
}
|
package/src/RuleParser.ebnf.js
CHANGED
|
@@ -30,7 +30,7 @@ END_ARRAY ::= WS* #x5D WS* /* ] right square bracket */
|
|
|
30
30
|
END_OBJECT ::= WS* #x7D WS* /* } right curly bracket */
|
|
31
31
|
NAME_SEPARATOR ::= WS* #x3A WS* /* : colon */
|
|
32
32
|
VALUE_SEPARATOR ::= WS* #x2C WS* /* , comma */
|
|
33
|
-
WS ::= [#x20#x09#x0A#x0D]
|
|
33
|
+
WS ::= [#x20#x09#x0A#x0D] /* Space | Tab | \n | \r */
|
|
34
34
|
|
|
35
35
|
operator ::= GTE | LTE | GT | LT | EQ | NEQ
|
|
36
36
|
eq_operator ::= EQ | NEQ
|
|
@@ -41,8 +41,8 @@ END_ARGUMENT ::= ")"
|
|
|
41
41
|
BEGIN_PARENTHESIS ::= "("
|
|
42
42
|
END_PARENTHESIS ::= ")"
|
|
43
43
|
|
|
44
|
-
argument ::= statement WS*
|
|
45
|
-
arguments ::= argument*
|
|
44
|
+
argument ::= statement WS*
|
|
45
|
+
arguments ::= (argument (WS* "," WS* argument)*)?
|
|
46
46
|
fname ::= [a-zA-z0-9]+
|
|
47
47
|
fcall ::= fname WS* BEGIN_ARGUMENT WS* arguments? END_ARGUMENT
|
|
48
48
|
|
package/src/RuleParser.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const {Parser} = require('ebnf/dist/Parser.js'),
|
|
2
|
+
{ParsingError} = require('ebnf'),
|
|
2
3
|
assert = require('assert')
|
|
3
4
|
|
|
4
5
|
let ParserRules = require('./RuleParser.ebnf.js')
|
|
5
6
|
let ParserCache;
|
|
6
7
|
|
|
8
|
+
const { ErrorAnalyzer } = require('./errors/ErrorAnalyzer');
|
|
9
|
+
|
|
7
10
|
const ArithmeticOperators = {
|
|
8
11
|
"+": 'MathAdd',
|
|
9
12
|
"-": 'MathSub',
|
|
@@ -68,11 +71,22 @@ class RuleParser {
|
|
|
68
71
|
ParserCache = new Parser(ParserRules, {debug: false})
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
try {
|
|
75
|
+
ret = ParserCache.getAST(txt.trim(), 'statement_main');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// If ebnf throws ParsingError, convert it to RuleParseError with helpful error code
|
|
78
|
+
if (e instanceof ParsingError) {
|
|
79
|
+
throw ErrorAnalyzer.analyzeParseFailure(txt, e);
|
|
80
|
+
}
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
72
83
|
|
|
73
84
|
if(ret){
|
|
74
85
|
return ret.children[0]
|
|
75
86
|
}
|
|
87
|
+
|
|
88
|
+
// If parsing failed without throwing (shouldn't happen with new ebnf), throw error
|
|
89
|
+
throw ErrorAnalyzer.analyzeParseFailure(txt);
|
|
76
90
|
}
|
|
77
91
|
static _parseArgument(argument){
|
|
78
92
|
assert(argument.type === 'argument')
|
|
@@ -498,9 +512,72 @@ class RuleParser {
|
|
|
498
512
|
}
|
|
499
513
|
}
|
|
500
514
|
static toIL(txt){
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
515
|
+
try {
|
|
516
|
+
const ast = RuleParser.toAst(txt)
|
|
517
|
+
if(!ast) throw new Error(`failed to parse ${txt}`)
|
|
518
|
+
return RuleParser._buildExpressionGroup(ast)
|
|
519
|
+
} catch (e) {
|
|
520
|
+
// If it's already a RuleParseError, just re-throw it
|
|
521
|
+
if (e.name === 'RuleParseError') {
|
|
522
|
+
throw e;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Check if it's a validation error we can map to a specific code
|
|
526
|
+
if (e.message && e.message.includes('Invalid time of day')) {
|
|
527
|
+
// Extract the invalid time from the error message
|
|
528
|
+
const match = e.message.match(/Invalid time of day[,:]?\s*([0-9:]+)/);
|
|
529
|
+
const badTod = match ? match[1] : 'invalid';
|
|
530
|
+
const { ParsingError } = require('ebnf');
|
|
531
|
+
const { RuleParseError } = require('./errors/RuleParseError');
|
|
532
|
+
|
|
533
|
+
// Calculate position (simplified - at end of input)
|
|
534
|
+
const lines = txt.trim().split('\n');
|
|
535
|
+
const position = {
|
|
536
|
+
line: lines.length,
|
|
537
|
+
column: lines[lines.length - 1].length + 1,
|
|
538
|
+
offset: txt.trim().length
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
throw new RuleParseError(
|
|
542
|
+
"BAD_TOD",
|
|
543
|
+
`Invalid time of day: ${badTod}`,
|
|
544
|
+
"Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
|
|
545
|
+
position,
|
|
546
|
+
badTod,
|
|
547
|
+
["HH:MM"],
|
|
548
|
+
txt.trim().substring(Math.max(0, txt.trim().length - 50))
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Check if it's a day of week error
|
|
553
|
+
if (e.message && e.message.includes('Invalid day of week')) {
|
|
554
|
+
const match = e.message.match(/Invalid day of week[,:]?\s*(\w+)/);
|
|
555
|
+
const badDow = match ? match[1] : 'invalid';
|
|
556
|
+
const { RuleParseError } = require('./errors/RuleParseError');
|
|
557
|
+
|
|
558
|
+
const lines = txt.trim().split('\n');
|
|
559
|
+
const position = {
|
|
560
|
+
line: lines.length,
|
|
561
|
+
column: lines[lines.length - 1].length + 1,
|
|
562
|
+
offset: txt.trim().length
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
throw new RuleParseError(
|
|
566
|
+
"BAD_DOW",
|
|
567
|
+
`Invalid day of week: ${badDow}`,
|
|
568
|
+
"Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
|
|
569
|
+
position,
|
|
570
|
+
badDow,
|
|
571
|
+
["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
|
|
572
|
+
txt.trim().substring(Math.max(0, txt.trim().length - 50))
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// For other errors, re-throw
|
|
577
|
+
throw e;
|
|
578
|
+
}
|
|
504
579
|
}
|
|
505
580
|
}
|
|
506
581
|
module.exports = RuleParser
|
|
582
|
+
module.exports.ParsingError = require('ebnf').ParsingError
|
|
583
|
+
module.exports.RuleParseError = require('./errors/RuleParseError').RuleParseError
|
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports=[{"name":"statement_main","bnf":[["statement","EOF"]]},{"name":"logical_operator","bnf":[["AND"],["OR"]]},{"name":"%
|
|
1
|
+
module.exports=[{"name":"statement_main","bnf":[["statement","EOF"]]},{"name":"logical_operator","bnf":[["AND"],["OR"]]},{"name":"%statement[2]","bnf":[["logical_operator","expression"]],"fragment":true},{"name":"statement","bnf":[["expression","%statement[2]*"]]},{"name":"expression","bnf":[["not_expression"],["standard_expression"],["parenthesis_expression"]]},{"name":"parenthesis_expression","bnf":[["BEGIN_PARENTHESIS","WS*","statement","WS*","END_PARENTHESIS"]]},{"name":"%not_expression[2]","bnf":[["result"],["parenthesis_expression"]],"fragment":true},{"name":"not_expression","bnf":[["NOT","%not_expression[2]"]]},{"name":"%standard_expression[2][1]","bnf":[["WS*","eq_approx"]],"fragment":true},{"name":"%standard_expression[2][2]","bnf":[["WS*","basic_rhs"]],"fragment":true},{"name":"%standard_expression[2][3][1]","bnf":[["WS+","IS"]],"fragment":true},{"name":"%standard_expression[2][3]","bnf":[["%standard_expression[2][3][1]?","WS+","between"]],"fragment":true},{"name":"%standard_expression[2]","bnf":[["%standard_expression[2][1]"],["%standard_expression[2][2]"],["%standard_expression[2][3]"]],"fragment":true},{"name":"standard_expression","bnf":[["result","%standard_expression[2]?"]]},{"name":"basic_rhs","bnf":[["operator","WS*","result"]]},{"name":"eq_approx","bnf":[["eq_operator","WS*","\"~\"","WS*","result"]]},{"name":"PLUS","bnf":[["\"+\""]]},{"name":"MINUS","bnf":[["\"-\""]]},{"name":"MULTIPLY","bnf":[["\"*\""]]},{"name":"DIVIDE","bnf":[["\"/\""]]},{"name":"MODULUS","bnf":[["\"%\""]]},{"name":"DEFAULT_VAL","bnf":[["\"??\""]]},{"name":"arithmetic_operator","bnf":[["PLUS"],["MINUS"],["MULTIPLY"],["DIVIDE"],["MODULUS"],["DEFAULT_VAL"]]},{"name":"arithmetic_operand","bnf":[["fcall"],["number_time"],["number"]]},{"name":"%arithmetic_result[5]","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","WS*","arithmetic_operator","WS*","%arithmetic_result[5]"]]},{"name":"simple_result","bnf":[["fcall"],["value"]]},{"name":"result","bnf":[["arithmetic_result"],["simple_result"]]},{"name":"value","bnf":[["false"],["true"],["array"],["time_period"],["number_time"],["number"],["number_tod"],["string"]]},{"name":"BEGIN_ARRAY","bnf":[["WS*",/\x5B/,"WS*"]]},{"name":"BEGIN_OBJECT","bnf":[["WS*",/\x7B/,"WS*"]]},{"name":"END_ARRAY","bnf":[["WS*",/\x5D/,"WS*"]]},{"name":"END_OBJECT","bnf":[["WS*",/\x7D/,"WS*"]]},{"name":"NAME_SEPARATOR","bnf":[["WS*",/\x3A/,"WS*"]]},{"name":"VALUE_SEPARATOR","bnf":[["WS*",/\x2C/,"WS*"]]},{"name":"WS","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"name":"operator","bnf":[["GTE"],["LTE"],["GT"],["LT"],["EQ"],["NEQ"]]},{"name":"eq_operator","bnf":[["EQ"],["NEQ"]]},{"name":"BEGIN_ARGUMENT","bnf":[["\"(\""]]},{"name":"END_ARGUMENT","bnf":[["\")\""]]},{"name":"BEGIN_PARENTHESIS","bnf":[["\"(\""]]},{"name":"END_PARENTHESIS","bnf":[["\")\""]]},{"name":"argument","bnf":[["statement","WS*"]]},{"name":"%arguments[1][2]","bnf":[["WS*","\",\"","WS*","argument"]],"fragment":true},{"name":"%arguments[1]","bnf":[["argument","%arguments[1][2]*"]],"fragment":true},{"name":"arguments","bnf":[["%arguments[1]?"]]},{"name":"%fname[1]","bnf":[[/[a-zA-z0-9]/]]},{"name":"fname","bnf":[["%fname[1]+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_number[1]","bnf":[["number_time"],["number"]],"fragment":true},{"name":"%between_number[2][1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_number[2][2]","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number[2]","bnf":[["%between_number[2][1]"],["%between_number[2][2]"]],"fragment":true},{"name":"%between_number[3]","bnf":[["number_time"],["number"]],"fragment":true},{"name":"between_number","bnf":[["%between_number[1]","%between_number[2]","%between_number[3]"]]},{"name":"%between_number_time[2][1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_number_time[2][2]","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number_time[2]","bnf":[["%between_number_time[2][1]"],["%between_number_time[2][2]"]],"fragment":true},{"name":"%between_number_time[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_number_time","bnf":[["number_time","%between_number_time[2]","number_time","%between_number_time[4]?"]]},{"name":"%between_tod[2][1]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod[2]","bnf":[["%between_tod[2][1]"]],"fragment":true},{"name":"%between_tod[4]","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["number_tod","%between_tod[2]","number_tod","%between_tod[4]?"]]},{"name":"%between[3]","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between[3]"]]},{"name":"dow","bnf":[[/[Mm]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Oo]/,/[Nn]/],[/[Tt]/,/[Uu]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Uu]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Dd]/,/[Nn]/,/[Ee]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ww]/,/[Ee]/,/[Dd]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/,/[Ss]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Tt]/,/[Hh]/,/[Uu]/],[/[Tt]/,/[Hh]/,/[Uu]/,/[Rr]/],[/[Ff]/,/[Rr]/,/[Ii]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ff]/,/[Rr]/,/[Ii]/],[/[Ss]/,/[Aa]/,/[Tt]/,/[Uu]/,/[Rr]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Aa]/,/[Tt]/],[/[Ss]/,/[Uu]/,/[Nn]/,/[Dd]/,/[Aa]/,/[Yy]/],[/[Ss]/,/[Uu]/,/[Nn]/]]},{"name":"%dow_range[4]","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow","%dow_range[4]?"]]},{"name":"between_time_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_number_time"]]},{"name":"between_tod_only","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","between_tod"]]},{"name":"%AND[1]","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND[2]","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND[1]"],["%AND[2]"]]},{"name":"%OR[1]","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR[2]","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR[1]"],["%OR[2]"]]},{"name":"AGO","bnf":[[/[Aa]/,/[Gg]/,/[Oo]/]]},{"name":"GT","bnf":[["\">\""]]},{"name":"LT","bnf":[["\"<\""]]},{"name":"GTE","bnf":[["\">=\""]]},{"name":"LTE","bnf":[["\"<=\""]]},{"name":"IS","bnf":[[/[Ii]/,/[Ss]/]]},{"name":"EQ","bnf":[["\"==\""],["\"=\""]]},{"name":"NEQ","bnf":[["\"!=\""]]},{"name":"%NOT[1]","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT[2]","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT[1]"],["%NOT[2]"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%array[2][2]","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array[2]","bnf":[["value","%array[2][2]*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array[2]?","END_ARRAY"]]},{"name":"unit","bnf":[[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/,/[Ss]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/,/[Ss]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/,/[Ss]/],[/[Dd]/,/[Aa]/,/[Yy]/,/[Ss]/],[/[Ss]/,/[Ee]/,/[Cc]/,/[Oo]/,/[Nn]/,/[Dd]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Uu]/,/[Tt]/,/[Ee]/],[/[Ww]/,/[Ee]/,/[Ee]/,/[Kk]/],[/[Hh]/,/[Oo]/,/[Uu]/,/[Rr]/],[/[Dd]/,/[Aa]/,/[Yy]/],[/[Mm]/,/[Ii]/,/[Nn]/,/[Ss]/],[/[Mm]/,/[Ii]/,/[Nn]/]]},{"name":"%number[2][1]","bnf":[[/[0-9]/]]},{"name":"%number[2]","bnf":[["%number[2][1]+"]],"fragment":true},{"name":"%number[3][2]","bnf":[[/[0-9]/]]},{"name":"%number[3]","bnf":[["\".\"","%number[3][2]+"]],"fragment":true},{"name":"%number[4][2]","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%number[4][3][2]","bnf":[[/[0-9]/]]},{"name":"%number[4][3]","bnf":[["\"0\""],[/[1-9]/,"%number[4][3][2]*"]],"fragment":true},{"name":"%number[4]","bnf":[["\"e\"","%number[4][2]?","%number[4][3]"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number[2]","%number[3]?","%number[4]?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%number_tod[1][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[1]","bnf":[["%number_tod[1][1]+"]],"fragment":true},{"name":"%number_tod[3][1]","bnf":[[/[0-9]/]]},{"name":"%number_tod[3]","bnf":[["%number_tod[3][1]+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod[1]","\":\"","%number_tod[3]"]]},{"name":"%time_period_ago[2]","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time","%time_period_ago[2]*","WS+","AGO"]]},{"name":"%time_period_ago_between[2]","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time","%time_period_ago_between[2]*","WS+","AGO","WS+","between_tod_only"]]},{"name":"time_period_const","bnf":[[/[Tt]/,/[Oo]/,/[Dd]/,/[Aa]/,/[Yy]/],["time_period_ago"]]},{"name":"time_period","bnf":[["time_period_ago_between"],["time_period_const"],["between_tod_only"],["between_time_only"]]},{"name":"%string[2][1]","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%string[2][2]","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string[2]","bnf":[["%string[2][1]"],[/\x5C/,"%string[2][2]"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string[2]*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const {Parser} = require('ebnf/dist/Parser.js'),
|
|
2
|
+
{ParsingError} = require('ebnf'),
|
|
2
3
|
assert = require('assert')
|
|
3
4
|
|
|
4
5
|
let ParserRules = require('./RuleParser.production.ebnf.js')
|
|
5
6
|
let ParserCache;
|
|
6
7
|
|
|
8
|
+
const { ErrorAnalyzer } = require('./errors/ErrorAnalyzer');
|
|
9
|
+
|
|
7
10
|
const ArithmeticOperators = {
|
|
8
11
|
"+": 'MathAdd',
|
|
9
12
|
"-": 'MathSub',
|
|
@@ -68,11 +71,22 @@ class RuleParser {
|
|
|
68
71
|
ParserCache = new Parser(ParserRules, {debug: false})
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
try {
|
|
75
|
+
ret = ParserCache.getAST(txt.trim(), 'statement_main');
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// If ebnf throws ParsingError, convert it to RuleParseError with helpful error code
|
|
78
|
+
if (e instanceof ParsingError) {
|
|
79
|
+
throw ErrorAnalyzer.analyzeParseFailure(txt, e);
|
|
80
|
+
}
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
72
83
|
|
|
73
84
|
if(ret){
|
|
74
85
|
return ret.children[0]
|
|
75
86
|
}
|
|
87
|
+
|
|
88
|
+
// If parsing failed without throwing (shouldn't happen with new ebnf), throw error
|
|
89
|
+
throw ErrorAnalyzer.analyzeParseFailure(txt);
|
|
76
90
|
}
|
|
77
91
|
static _parseArgument(argument){
|
|
78
92
|
assert(argument.type === 'argument')
|
|
@@ -498,9 +512,72 @@ class RuleParser {
|
|
|
498
512
|
}
|
|
499
513
|
}
|
|
500
514
|
static toIL(txt){
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
515
|
+
try {
|
|
516
|
+
const ast = RuleParser.toAst(txt)
|
|
517
|
+
if(!ast) throw new Error(`failed to parse ${txt}`)
|
|
518
|
+
return RuleParser._buildExpressionGroup(ast)
|
|
519
|
+
} catch (e) {
|
|
520
|
+
// If it's already a RuleParseError, just re-throw it
|
|
521
|
+
if (e.name === 'RuleParseError') {
|
|
522
|
+
throw e;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Check if it's a validation error we can map to a specific code
|
|
526
|
+
if (e.message && e.message.includes('Invalid time of day')) {
|
|
527
|
+
// Extract the invalid time from the error message
|
|
528
|
+
const match = e.message.match(/Invalid time of day[,:]?\s*([0-9:]+)/);
|
|
529
|
+
const badTod = match ? match[1] : 'invalid';
|
|
530
|
+
const { ParsingError } = require('ebnf');
|
|
531
|
+
const { RuleParseError } = require('./errors/RuleParseError');
|
|
532
|
+
|
|
533
|
+
// Calculate position (simplified - at end of input)
|
|
534
|
+
const lines = txt.trim().split('\n');
|
|
535
|
+
const position = {
|
|
536
|
+
line: lines.length,
|
|
537
|
+
column: lines[lines.length - 1].length + 1,
|
|
538
|
+
offset: txt.trim().length
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
throw new RuleParseError(
|
|
542
|
+
"BAD_TOD",
|
|
543
|
+
`Invalid time of day: ${badTod}`,
|
|
544
|
+
"Time of day must be in HH:MM format with hours 0-23 and minutes 0-59, e.g. 08:30, 14:00, 23:59.",
|
|
545
|
+
position,
|
|
546
|
+
badTod,
|
|
547
|
+
["HH:MM"],
|
|
548
|
+
txt.trim().substring(Math.max(0, txt.trim().length - 50))
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Check if it's a day of week error
|
|
553
|
+
if (e.message && e.message.includes('Invalid day of week')) {
|
|
554
|
+
const match = e.message.match(/Invalid day of week[,:]?\s*(\w+)/);
|
|
555
|
+
const badDow = match ? match[1] : 'invalid';
|
|
556
|
+
const { RuleParseError } = require('./errors/RuleParseError');
|
|
557
|
+
|
|
558
|
+
const lines = txt.trim().split('\n');
|
|
559
|
+
const position = {
|
|
560
|
+
line: lines.length,
|
|
561
|
+
column: lines[lines.length - 1].length + 1,
|
|
562
|
+
offset: txt.trim().length
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
throw new RuleParseError(
|
|
566
|
+
"BAD_DOW",
|
|
567
|
+
`Invalid day of week: ${badDow}`,
|
|
568
|
+
"Valid days are: MONDAY/MON, TUESDAY/TUE, WEDNESDAY/WED, THURSDAY/THU, FRIDAY/FRI, SATURDAY/SAT, SUNDAY/SUN.",
|
|
569
|
+
position,
|
|
570
|
+
badDow,
|
|
571
|
+
["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"],
|
|
572
|
+
txt.trim().substring(Math.max(0, txt.trim().length - 50))
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// For other errors, re-throw
|
|
577
|
+
throw e;
|
|
578
|
+
}
|
|
504
579
|
}
|
|
505
580
|
}
|
|
506
581
|
module.exports = RuleParser
|
|
582
|
+
module.exports.ParsingError = require('ebnf').ParsingError
|
|
583
|
+
module.exports.RuleParseError = require('./errors/RuleParseError').RuleParseError
|