@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 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.13",
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
  }
@@ -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]+ /* Space | Tab | \n | \r */
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* ("," 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
- ret = ParserCache.getAST(txt.trim(), 'statement_main');
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
- const ast = RuleParser.toAst(txt)
502
- if(!ast) throw new Error(`failed to parse ${txt}`)
503
- return RuleParser._buildExpressionGroup(ast)
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":"%statement0","bnf":[["logical_operator","expression"]],"fragment":true},{"name":"statement","bnf":[["expression","%statement0*"]]},{"name":"expression","bnf":[["not_expression"],["standard_expression"],["parenthesis_expression"]]},{"name":"parenthesis_expression","bnf":[["BEGIN_PARENTHESIS","WS*","statement","WS*","END_PARENTHESIS"]]},{"name":"%not_expression1","bnf":[["result"],["parenthesis_expression"]],"fragment":true},{"name":"not_expression","bnf":[["NOT","%not_expression1"]]},{"name":"%%standard_expression23","bnf":[["WS*","eq_approx"]],"fragment":true},{"name":"%%standard_expression24","bnf":[["WS*","basic_rhs"]],"fragment":true},{"name":"%%%standard_expression256","bnf":[["WS+","IS"]],"fragment":true},{"name":"%%standard_expression25","bnf":[["%%%standard_expression256?","WS+","between"]],"fragment":true},{"name":"%standard_expression2","bnf":[["%%standard_expression23"],["%%standard_expression24"],["%%standard_expression25"]],"fragment":true},{"name":"standard_expression","bnf":[["result","%standard_expression2?"]]},{"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_result7","bnf":[["arithmetic_result"],["arithmetic_operand"]],"fragment":true},{"name":"arithmetic_result","bnf":[["arithmetic_operand","WS*","arithmetic_operator","WS*","%arithmetic_result7"]]},{"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":"%WS8","bnf":[[/[\x20\x09\x0A\x0D]/]]},{"name":"WS","bnf":[["%WS8+"]]},{"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":"%argument9","bnf":[["\",\"","WS*"]],"fragment":true},{"name":"argument","bnf":[["statement","WS*","%argument9?"]]},{"name":"arguments","bnf":[["argument*"]]},{"name":"%fname10","bnf":[[/[a-zA-z0-9]/]]},{"name":"fname","bnf":[["%fname10+"]]},{"name":"fcall","bnf":[["fname","WS*","BEGIN_ARGUMENT","WS*","arguments?","END_ARGUMENT"]]},{"name":"%between_number11","bnf":[["number_time"],["number"]],"fragment":true},{"name":"%%between_number1213","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%%between_number1214","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number12","bnf":[["%%between_number1213"],["%%between_number1214"]],"fragment":true},{"name":"%between_number15","bnf":[["number_time"],["number"]],"fragment":true},{"name":"between_number","bnf":[["%between_number11","%between_number12","%between_number15"]]},{"name":"%%between_number_time1617","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%%between_number_time1618","bnf":[["WS*",/\-/,"WS*"]],"fragment":true},{"name":"%between_number_time16","bnf":[["%%between_number_time1617"],["%%between_number_time1618"]],"fragment":true},{"name":"%between_number_time19","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_number_time","bnf":[["number_time","%between_number_time16","number_time","%between_number_time19?"]]},{"name":"%%between_tod2021","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"%between_tod20","bnf":[["%%between_tod2021"]],"fragment":true},{"name":"%between_tod22","bnf":[["WS+","dow_range"]],"fragment":true},{"name":"between_tod","bnf":[["number_tod","%between_tod20","number_tod","%between_tod22?"]]},{"name":"%between23","bnf":[["between_number"],["between_tod"]],"fragment":true},{"name":"between","bnf":[[/[Bb]/,/[Ee]/,/[Tt]/,/[Ww]/,/[Ee]/,/[Ee]/,/[Nn]/,"WS+","%between23"]]},{"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_range24","bnf":[["WS+",/[Tt]/,/[Oo]/,"WS+","dow"]],"fragment":true},{"name":"dow_range","bnf":[[/[Oo]/,/[Nn]/,"WS+","dow","%dow_range24?"]]},{"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":"%AND25","bnf":[["WS*",/&/,/&/,"WS*"]],"fragment":true},{"name":"%AND26","bnf":[["WS+",/[Aa]/,/[Nn]/,/[Dd]/,"WS+"]],"fragment":true},{"name":"AND","bnf":[["%AND25"],["%AND26"]]},{"name":"%OR27","bnf":[["WS*",/\|/,/\|/,"WS*"]],"fragment":true},{"name":"%OR28","bnf":[["WS+",/[Oo]/,/[Rr]/,"WS+"]],"fragment":true},{"name":"OR","bnf":[["%OR27"],["%OR28"]]},{"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":"%NOT29","bnf":[[/!/,"WS*"]],"fragment":true},{"name":"%NOT30","bnf":[[/[Nn]/,/[Oo]/,/[Tt]/,"WS+"]],"fragment":true},{"name":"NOT","bnf":[["%NOT29"],["%NOT30"]]},{"name":"false","bnf":[[/[Ff]/,/[Aa]/,/[Ll]/,/[Ss]/,/[Ee]/]]},{"name":"null","bnf":[[/[Nn]/,/[Uu]/,/[Ll]/,/[Ll]/]]},{"name":"true","bnf":[[/[Tt]/,/[Rr]/,/[Uu]/,/[Ee]/]]},{"name":"%%array3132","bnf":[["VALUE_SEPARATOR","value"]],"fragment":true},{"name":"%array31","bnf":[["value","%%array3132*"]],"fragment":true},{"name":"array","bnf":[["BEGIN_ARRAY","%array31?","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":"%%number3334","bnf":[[/[0-9]/]]},{"name":"%number33","bnf":[["%%number3334+"]],"fragment":true},{"name":"%%number3536","bnf":[[/[0-9]/]]},{"name":"%number35","bnf":[["\".\"","%%number3536+"]],"fragment":true},{"name":"%%number3738","bnf":[["\"-\""],["\"+\""]],"fragment":true},{"name":"%%%number373940","bnf":[[/[0-9]/]]},{"name":"%%number3739","bnf":[["\"0\""],[/[1-9]/,"%%%number373940*"]],"fragment":true},{"name":"%number37","bnf":[["\"e\"","%%number3738?","%%number3739"]],"fragment":true},{"name":"number","bnf":[["\"-\"?","%number33","%number35?","%number37?"]]},{"name":"number_time","bnf":[["number","WS+","unit"]]},{"name":"%%number_tod4142","bnf":[[/[0-9]/]]},{"name":"%number_tod41","bnf":[["%%number_tod4142+"]],"fragment":true},{"name":"%%number_tod4344","bnf":[[/[0-9]/]]},{"name":"%number_tod43","bnf":[["%%number_tod4344+"]],"fragment":true},{"name":"number_tod","bnf":[["%number_tod41","\":\"","%number_tod43"]]},{"name":"%time_period_ago45","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago","bnf":[["number_time","%time_period_ago45*","WS+","AGO"]]},{"name":"%time_period_ago_between46","bnf":[["WS+","number_time"]],"fragment":true},{"name":"time_period_ago_between","bnf":[["number_time","%time_period_ago_between46*","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":"%%string4748","bnf":[[/[\x20-\x21]/],[/[\x23-\x5B]/],[/[\x5D-\uFFFF]/]],"fragment":true},{"name":"%%string4749","bnf":[[/\x22/],[/\x5C/],[/\x2F/],[/\x62/],[/\x66/],[/\x6E/],[/\x72/],[/\x74/],[/\x75/,"HEXDIG","HEXDIG","HEXDIG","HEXDIG"]],"fragment":true},{"name":"%string47","bnf":[["%%string4748"],[/\x5C/,"%%string4749"]],"fragment":true},{"name":"string","bnf":[["'\"'","%string47*","'\"'"]]},{"name":"HEXDIG","bnf":[[/[a-fA-F0-9]/]]}]
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
- ret = ParserCache.getAST(txt.trim(), 'statement_main');
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
- const ast = RuleParser.toAst(txt)
502
- if(!ast) throw new Error(`failed to parse ${txt}`)
503
- return RuleParser._buildExpressionGroup(ast)
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