jprx 1.1.0 → 1.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jprx",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "JSON Reactive Path eXpressions - A reactive expression language for JSON data",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/parser.js CHANGED
@@ -487,16 +487,17 @@ const tokenize = (expr) => {
487
487
  // In expressions like "$++/count", the $ is just the JPRX delimiter
488
488
  // and ++ is a prefix operator applied to /count
489
489
  if (expr[i] === '$' && i + 1 < len) {
490
- // Check if next chars are an operator
491
- let isOpAfter = false;
492
- for (const op of opSymbols) {
490
+ // Check if next chars are a PREFIX operator (sort by length to match longest first)
491
+ const prefixOps = [...operators.prefix.keys()].sort((a, b) => b.length - a.length);
492
+ let isPrefixOp = false;
493
+ for (const op of prefixOps) {
493
494
  if (expr.slice(i + 1, i + 1 + op.length) === op) {
494
- isOpAfter = true;
495
+ isPrefixOp = true;
495
496
  break;
496
497
  }
497
498
  }
498
- if (isOpAfter) {
499
- // Skip the $, it's just a delimiter
499
+ if (isPrefixOp) {
500
+ // Skip the $, it's just a delimiter for a prefix operator (e.g., $++/count)
500
501
  i++;
501
502
  continue;
502
503
  }
@@ -814,7 +815,7 @@ class PrattParser {
814
815
  const prec = this.getInfixPrecedence(tok.value);
815
816
  if (prec < minPrecedence) break;
816
817
 
817
- // Check if it's a postfix operator
818
+ // Check if it's a postfix-only operator
818
819
  if (operators.postfix.has(tok.value) && !operators.infix.has(tok.value)) {
819
820
  this.consume();
820
821
  left = { type: 'Postfix', operator: tok.value, operand: left };
@@ -832,6 +833,12 @@ class PrattParser {
832
833
  continue;
833
834
  }
834
835
 
836
+ // Operator not registered as postfix or infix - treat as unknown and stop
837
+ // This prevents infinite loops when operators are tokenized but not registered
838
+ if (!operators.postfix.has(tok.value) && !operators.infix.has(tok.value)) {
839
+ break;
840
+ }
841
+
835
842
  // Postfix that's also infix - context determines
836
843
  // If next token is a value, treat as infix
837
844
  this.consume();
@@ -1019,7 +1026,18 @@ const evaluateAST = (ast, context, forMutation = false) => {
1019
1026
  // For infix, typically first arg might be pathAware
1020
1027
  const left = evaluateAST(ast.left, context, opts.pathAware);
1021
1028
  const right = evaluateAST(ast.right, context, false);
1022
- return helper(unwrapSignal(left), unwrapSignal(right));
1029
+
1030
+ const finalArgs = [];
1031
+
1032
+ // Handle potentially exploded arguments (like in sum(/items...p))
1033
+ // Although infix operators usually take exactly 2 args, we treat them consistently
1034
+ if (Array.isArray(left) && ast.left.type === 'Explosion') finalArgs.push(...left);
1035
+ else finalArgs.push(unwrapSignal(left));
1036
+
1037
+ if (Array.isArray(right) && ast.right.type === 'Explosion') finalArgs.push(...right);
1038
+ else finalArgs.push(unwrapSignal(right));
1039
+
1040
+ return helper(...finalArgs);
1023
1041
  }
1024
1042
 
1025
1043
  default:
@@ -0,0 +1,71 @@
1
+ [
2
+ {
3
+ "name": "Global Path Resolution",
4
+ "state": {
5
+ "userName": "Alice"
6
+ },
7
+ "expression": "$/userName",
8
+ "expected": "Alice"
9
+ },
10
+ {
11
+ "name": "Deep Global Path Resolution",
12
+ "state": {
13
+ "user": {
14
+ "profile": {
15
+ "name": "Bob"
16
+ }
17
+ }
18
+ },
19
+ "expression": "$/user/profile/name",
20
+ "expected": "Bob"
21
+ },
22
+ {
23
+ "name": "Relative Path Resolution",
24
+ "context": {
25
+ "age": 30
26
+ },
27
+ "expression": "./age",
28
+ "expected": 30
29
+ },
30
+ {
31
+ "name": "Math: Addition",
32
+ "state": {
33
+ "a": 5,
34
+ "b": 10
35
+ },
36
+ "expression": "$+(/a, /b)",
37
+ "expected": 15
38
+ },
39
+ {
40
+ "name": "Operator: Addition (Infix)",
41
+ "state": {
42
+ "a": 5,
43
+ "b": 10
44
+ },
45
+ "expression": "$/a + $/b",
46
+ "expected": 15
47
+ },
48
+ {
49
+ "name": "Logic: If (True)",
50
+ "state": {
51
+ "isVip": true
52
+ },
53
+ "expression": "$if(/isVip, \"Gold\", \"Silver\")",
54
+ "expected": "Gold"
55
+ },
56
+ {
57
+ "name": "Explosion Operator",
58
+ "state": {
59
+ "items": [
60
+ {
61
+ "p": 10
62
+ },
63
+ {
64
+ "p": 20
65
+ }
66
+ ]
67
+ },
68
+ "expression": "$sum(/items...p)",
69
+ "expected": 30
70
+ }
71
+ ]
@@ -0,0 +1,150 @@
1
+ [
2
+ {
3
+ "name": "Math: Subtraction",
4
+ "expression": "$-(10, 3)",
5
+ "expected": 7
6
+ },
7
+ {
8
+ "name": "Math: Multiplication",
9
+ "expression": "$*(4, 5)",
10
+ "expected": 20
11
+ },
12
+ {
13
+ "name": "Math: Division",
14
+ "expression": "$/(20, 4)",
15
+ "expected": 5
16
+ },
17
+ {
18
+ "name": "Math: Round",
19
+ "expression": "$round(3.7)",
20
+ "expected": 4
21
+ },
22
+ {
23
+ "name": "String: Upper",
24
+ "expression": "$upper('hello')",
25
+ "expected": "HELLO"
26
+ },
27
+ {
28
+ "name": "String: Lower",
29
+ "expression": "$lower('WORLD')",
30
+ "expected": "world"
31
+ },
32
+ {
33
+ "name": "String: Concat",
34
+ "expression": "$concat('Hello', ' ', 'World')",
35
+ "expected": "Hello World"
36
+ },
37
+ {
38
+ "name": "String: Trim",
39
+ "expression": "$trim(' spaced ')",
40
+ "expected": "spaced"
41
+ },
42
+ {
43
+ "name": "Logic: And (true)",
44
+ "expression": "$and(true, true)",
45
+ "expected": true
46
+ },
47
+ {
48
+ "name": "Logic: And (false)",
49
+ "expression": "$and(true, false)",
50
+ "expected": false
51
+ },
52
+ {
53
+ "name": "Logic: Or",
54
+ "expression": "$or(false, true)",
55
+ "expected": true
56
+ },
57
+ {
58
+ "name": "Logic: Not",
59
+ "expression": "$not(false)",
60
+ "expected": true
61
+ },
62
+ {
63
+ "name": "Compare: Greater Than",
64
+ "expression": "$gt(10, 5)",
65
+ "expected": true
66
+ },
67
+ {
68
+ "name": "Compare: Less Than",
69
+ "expression": "$lt(3, 7)",
70
+ "expected": true
71
+ },
72
+ {
73
+ "name": "Compare: Equal",
74
+ "expression": "$eq(5, 5)",
75
+ "expected": true
76
+ },
77
+ {
78
+ "name": "Conditional: If (true branch)",
79
+ "expression": "$if(true, 'Yes', 'No')",
80
+ "expected": "Yes"
81
+ },
82
+ {
83
+ "name": "Conditional: If (false branch)",
84
+ "expression": "$if(false, 'Yes', 'No')",
85
+ "expected": "No"
86
+ },
87
+ {
88
+ "name": "Stats: Average",
89
+ "expression": "$avg(10, 20, 30)",
90
+ "expected": 20
91
+ },
92
+ {
93
+ "name": "Stats: Min",
94
+ "expression": "$min(5, 2, 8)",
95
+ "expected": 2
96
+ },
97
+ {
98
+ "name": "Stats: Max",
99
+ "expression": "$max(5, 2, 8)",
100
+ "expected": 8
101
+ },
102
+ {
103
+ "name": "Array: Length",
104
+ "state": {
105
+ "items": [
106
+ "a",
107
+ "b",
108
+ "c"
109
+ ]
110
+ },
111
+ "expression": "$len(/items)",
112
+ "expected": 3
113
+ },
114
+ {
115
+ "name": "Array: Join",
116
+ "state": {
117
+ "items": [
118
+ "a",
119
+ "b",
120
+ "c"
121
+ ]
122
+ },
123
+ "expression": "$join(/items, '-')",
124
+ "expected": "a-b-c"
125
+ },
126
+ {
127
+ "name": "Array: First",
128
+ "state": {
129
+ "items": [
130
+ "a",
131
+ "b",
132
+ "c"
133
+ ]
134
+ },
135
+ "expression": "$first(/items)",
136
+ "expected": "a"
137
+ },
138
+ {
139
+ "name": "Array: Last",
140
+ "state": {
141
+ "items": [
142
+ "a",
143
+ "b",
144
+ "c"
145
+ ]
146
+ },
147
+ "expression": "$last(/items)",
148
+ "expected": "c"
149
+ }
150
+ ]