jilt 0.1.0
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/LICENSE +21 -0
- package/README.md +432 -0
- package/dist/ast.d.ts +62 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +2 -0
- package/dist/ast.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +69 -0
- package/dist/cli.js.map +1 -0
- package/dist/compiler.d.ts +17 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +792 -0
- package/dist/compiler.js.map +1 -0
- package/dist/help.d.ts +3 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +225 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +32 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +547 -0
- package/dist/parser.js.map +1 -0
- package/dist/stringify.d.ts +3 -0
- package/dist/stringify.d.ts.map +1 -0
- package/dist/stringify.js +153 -0
- package/dist/stringify.js.map +1 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sandro Hawke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# jilt - JSON object filtering
|
|
2
|
+
|
|
3
|
+
Toolkit for turning human-readable/human-writable filter expressions
|
|
4
|
+
into executable functions which check if a JSON object matches.
|
|
5
|
+
|
|
6
|
+
Vibe coded by codex using model gpt-5.2-codex.
|
|
7
|
+
|
|
8
|
+
## Quickstart
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import jilt from '@walkable/jilt'
|
|
12
|
+
|
|
13
|
+
const expr1 = jilt.parse('color = "red" and weight < 3.2 and size.x > 1')
|
|
14
|
+
|
|
15
|
+
console.log(expr1.tree())
|
|
16
|
+
// ['and', ['eq', ['field', 'color'], ['literal', 'red']], ...]
|
|
17
|
+
|
|
18
|
+
const func1 = expr1.compile()
|
|
19
|
+
|
|
20
|
+
console.log(func1({color: 'red', weight: 3, size: {x: 2, y: 2}}))
|
|
21
|
+
// => true
|
|
22
|
+
|
|
23
|
+
console.log(func1({color: 'red', weight: 3, size: {x: 1, y: 2}}))
|
|
24
|
+
// => false
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## CLI
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Test an expression against JSON input
|
|
31
|
+
echo '{"color": "red", "weight": 3}' | jilt 'color = "red" and weight < 5'
|
|
32
|
+
# => true
|
|
33
|
+
|
|
34
|
+
# Parse and display the AST
|
|
35
|
+
jilt --tree 'color = "red" and weight < 5'
|
|
36
|
+
# => ['and', ['eq', ['field', 'color'], ['literal', 'red']], ['lt', ['field', 'weight'], ['literal', 5]]]
|
|
37
|
+
|
|
38
|
+
# Interactive mode - enter JSON objects, see if they match
|
|
39
|
+
jilt -i 'price < 100 and inStock = true'
|
|
40
|
+
> {"price": 50, "inStock": true}
|
|
41
|
+
true
|
|
42
|
+
> {"price": 150, "inStock": true}
|
|
43
|
+
false
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Expression Syntax
|
|
47
|
+
|
|
48
|
+
### Literals
|
|
49
|
+
|
|
50
|
+
| Type | Examples |
|
|
51
|
+
|------|----------|
|
|
52
|
+
| String | `"hello"`, `'hello'` |
|
|
53
|
+
| Number | `42`, `3.14`, `-10`, `2.5e10` |
|
|
54
|
+
| Boolean | `true`, `false` |
|
|
55
|
+
| Null | `null` |
|
|
56
|
+
|
|
57
|
+
### Field Access
|
|
58
|
+
|
|
59
|
+
Access object properties using dot notation or bracket notation:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
name # top-level field
|
|
63
|
+
user.name # nested field
|
|
64
|
+
user.address.city # deeply nested
|
|
65
|
+
items[0] # array index
|
|
66
|
+
items[0].name # array index then field
|
|
67
|
+
user["first name"] # bracket notation for special characters
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Comparison Operators
|
|
71
|
+
|
|
72
|
+
| Operator | Aliases | Description |
|
|
73
|
+
|----------|---------|-------------|
|
|
74
|
+
| `=` | `==`, `eq` | Equal |
|
|
75
|
+
| `!=` | `<>`, `ne` | Not equal |
|
|
76
|
+
| `<` | `lt` | Less than |
|
|
77
|
+
| `<=` | `le` | Less than or equal |
|
|
78
|
+
| `>` | `gt` | Greater than |
|
|
79
|
+
| `>=` | `ge` | Greater than or equal |
|
|
80
|
+
|
|
81
|
+
### Logical Operators
|
|
82
|
+
|
|
83
|
+
| Operator | Description |
|
|
84
|
+
|----------|-------------|
|
|
85
|
+
| `and` | Logical AND |
|
|
86
|
+
| `or` | Logical OR |
|
|
87
|
+
| `not` | Logical NOT |
|
|
88
|
+
|
|
89
|
+
Parentheses control precedence:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
(color = "red" or color = "blue") and weight < 5
|
|
93
|
+
not (status = "deleted")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Arithmetic Operators
|
|
97
|
+
|
|
98
|
+
| Operator | Description |
|
|
99
|
+
|----------|-------------|
|
|
100
|
+
| `+` | Addition |
|
|
101
|
+
| `-` | Subtraction |
|
|
102
|
+
| `*` | Multiplication |
|
|
103
|
+
| `/` | Division |
|
|
104
|
+
| `%` | Modulo |
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
price * quantity > 100
|
|
108
|
+
(width + height) / 2 >= 10
|
|
109
|
+
count % 2 = 0
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### String Functions
|
|
113
|
+
|
|
114
|
+
| Function | Description | Example |
|
|
115
|
+
|----------|-------------|---------|
|
|
116
|
+
| `length(s)` | String length | `length(name) > 5` |
|
|
117
|
+
| `upper(s)` | Uppercase | `upper(status) = "ACTIVE"` |
|
|
118
|
+
| `lower(s)` | Lowercase | `lower(email) = "test@example.com"` |
|
|
119
|
+
| `trim(s)` | Remove leading/trailing whitespace | `trim(input) != ""` |
|
|
120
|
+
| `contains(s, sub)` | Check if string contains substring | `contains(title, "urgent")` |
|
|
121
|
+
| `startsWith(s, prefix)` | Check if string starts with prefix | `startsWith(id, "usr_")` |
|
|
122
|
+
| `endsWith(s, suffix)` | Check if string ends with suffix | `endsWith(filename, ".json")` |
|
|
123
|
+
| `substr(s, start)` | Substring from start index | `substr(code, 0, 2) = "US"` |
|
|
124
|
+
| `substr(s, start, len)` | Substring with length | `substr(code, 0, 2) = "US"` |
|
|
125
|
+
| `concat(s1, s2, ...)` | Concatenate strings | `concat(first, " ", last)` |
|
|
126
|
+
| `replace(s, old, new)` | Replace first occurrence | `replace(path, "/old/", "/new/")` |
|
|
127
|
+
| `split(s, delim)` | Split string into array | `length(split(tags, ",")) > 2` |
|
|
128
|
+
|
|
129
|
+
### Pattern Matching
|
|
130
|
+
|
|
131
|
+
| Function | Description | Example |
|
|
132
|
+
|----------|-------------|---------|
|
|
133
|
+
| `regex(s, pattern)` | Match against regular expression | `regex(email, "^[a-z]+@[a-z]+\\.[a-z]+$")` |
|
|
134
|
+
| `like(s, pattern)` | SQL-style wildcards: `%` = any chars, `_` = single char | `like(email, "%@gmail.com")` |
|
|
135
|
+
| `glob(s, pattern)` | Shell-style wildcards: `*` = any chars, `?` = single char | `glob(filename, "*.json")` |
|
|
136
|
+
|
|
137
|
+
Pattern matching examples:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
# SQL-style LIKE
|
|
141
|
+
like(name, "John%") # starts with "John"
|
|
142
|
+
like(code, "A_B") # "A" + any single char + "B"
|
|
143
|
+
like(email, "%@%.com") # contains "@" and ends with ".com"
|
|
144
|
+
|
|
145
|
+
# Shell-style glob
|
|
146
|
+
glob(file, "*.txt") # ends with ".txt"
|
|
147
|
+
glob(path, "/home/*/docs") # any user's docs folder
|
|
148
|
+
glob(name, "test?.js") # test1.js, testA.js, etc.
|
|
149
|
+
|
|
150
|
+
# Regular expressions
|
|
151
|
+
regex(phone, "^\\d{3}-\\d{4}$") # 123-4567
|
|
152
|
+
regex(id, "^[A-Z]{2}\\d{6}$") # US123456
|
|
153
|
+
regex(version, "^v\\d+\\.\\d+\\.\\d+$") # v1.2.3
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Path Functions
|
|
157
|
+
|
|
158
|
+
| Function | Description | Example |
|
|
159
|
+
|----------|-------------|---------|
|
|
160
|
+
| `basename(path)` | Get filename from path | `basename("/foo/bar.txt") = "bar.txt"` |
|
|
161
|
+
| `dirname(path)` | Get directory from path | `dirname("/foo/bar.txt") = "/foo"` |
|
|
162
|
+
| `ext(path)` | Get file extension (without dot) | `ext("/foo/bar.txt") = "txt"` |
|
|
163
|
+
|
|
164
|
+
Path function examples:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
ext(filename) = "json"
|
|
168
|
+
basename(path) = "config.yaml"
|
|
169
|
+
dirname(file) = "/etc/myapp"
|
|
170
|
+
startsWith(dirname(path), "/home/admin")
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Array Functions
|
|
174
|
+
|
|
175
|
+
| Function | Description | Example |
|
|
176
|
+
|----------|-------------|---------|
|
|
177
|
+
| `length(arr)` | Array length | `length(items) > 0` |
|
|
178
|
+
| `contains(arr, val)` | Check if array contains value | `contains(tags, "featured")` |
|
|
179
|
+
| `first(arr)` | First element | `first(scores) > 90` |
|
|
180
|
+
| `last(arr)` | Last element | `last(events).type = "complete"` |
|
|
181
|
+
| `at(arr, index)` | Element at index | `at(items, 2).price < 10` |
|
|
182
|
+
| `sum(arr)` | Sum of numeric array | `sum(prices) < 1000` |
|
|
183
|
+
| `avg(arr)` | Average of numeric array | `avg(scores) >= 70` |
|
|
184
|
+
| `min(arr)` | Minimum value | `min(bids) > 100` |
|
|
185
|
+
| `max(arr)` | Maximum value | `max(temperatures) < 30` |
|
|
186
|
+
| `join(arr, delim)` | Join array into string | `join(names, ", ")` |
|
|
187
|
+
| `reverse(arr)` | Reverse array | `first(reverse(items))` |
|
|
188
|
+
| `sort(arr)` | Sort array ascending | `first(sort(scores))` |
|
|
189
|
+
| `unique(arr)` | Remove duplicates | `length(unique(tags)) = length(tags)` |
|
|
190
|
+
|
|
191
|
+
### Array Predicates
|
|
192
|
+
|
|
193
|
+
Test conditions across array elements using `@` to reference the current element:
|
|
194
|
+
|
|
195
|
+
| Function | Description | Example |
|
|
196
|
+
|----------|-------------|---------|
|
|
197
|
+
| `any(arr, pred)` | True if any element matches | `any(items, @.price > 100)` |
|
|
198
|
+
| `all(arr, pred)` | True if all elements match | `all(scores, @ >= 60)` |
|
|
199
|
+
| `none(arr, pred)` | True if no elements match | `none(items, @.status = "error")` |
|
|
200
|
+
| `count(arr, pred)` | Count matching elements | `count(items, @.inStock) > 5` |
|
|
201
|
+
| `filter(arr, pred)` | Filter to matching elements | `length(filter(users, @.active)) > 0` |
|
|
202
|
+
| `map(arr, expr)` | Transform elements | `sum(map(items, @.price * @.qty))` |
|
|
203
|
+
|
|
204
|
+
### Type Checking
|
|
205
|
+
|
|
206
|
+
| Function | Description | Example |
|
|
207
|
+
|----------|-------------|---------|
|
|
208
|
+
| `type(val)` | Get type as string | `type(data) = "array"` |
|
|
209
|
+
| `isString(val)` | Check if string | `isString(name)` |
|
|
210
|
+
| `isNumber(val)` | Check if number | `isNumber(count)` |
|
|
211
|
+
| `isBool(val)` | Check if boolean | `isBool(enabled)` |
|
|
212
|
+
| `isNull(val)` | Check if null | `isNull(deletedAt)` |
|
|
213
|
+
| `isArray(val)` | Check if array | `isArray(tags)` |
|
|
214
|
+
| `isObject(val)` | Check if object | `isObject(metadata)` |
|
|
215
|
+
|
|
216
|
+
### Conditional
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
if(condition, thenValue, elseValue)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
```
|
|
224
|
+
if(premium, price * 0.9, price) < 50
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Null Handling
|
|
228
|
+
|
|
229
|
+
| Operator | Description | Example |
|
|
230
|
+
|----------|-------------|---------|
|
|
231
|
+
| `??` | Null coalescing | `(nickname ?? name) = "Admin"` |
|
|
232
|
+
| `?.` | Optional chaining | `user?.profile?.avatar != null` |
|
|
233
|
+
|
|
234
|
+
## Operator Precedence
|
|
235
|
+
|
|
236
|
+
From highest to lowest:
|
|
237
|
+
|
|
238
|
+
1. Parentheses `()`
|
|
239
|
+
2. Function calls, field access `.`, `[]`, `?.`
|
|
240
|
+
3. Unary `not`, `-`
|
|
241
|
+
4. Multiplicative `*`, `/`, `%`
|
|
242
|
+
5. Additive `+`, `-`
|
|
243
|
+
6. Comparison `=`, `!=`, `<`, `<=`, `>`, `>=`
|
|
244
|
+
7. Logical `and`
|
|
245
|
+
8. Logical `or`
|
|
246
|
+
9. Null coalescing `??`
|
|
247
|
+
|
|
248
|
+
## Complex Examples
|
|
249
|
+
|
|
250
|
+
### Computed string comparison
|
|
251
|
+
|
|
252
|
+
Check if concatenated name fields match the full name:
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
concat(firstName, " ", lastName) = fullName
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
With case-insensitive comparison:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
lower(concat(firstName, " ", lastName)) = lower(fullName)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Validating data consistency
|
|
265
|
+
|
|
266
|
+
Check that line items sum to the total:
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
sum(map(items, @.price * @.quantity)) = orderTotal
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Verify no duplicate IDs in an array:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
length(unique(map(items, @.id))) = length(items)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Complex filtering conditions
|
|
279
|
+
|
|
280
|
+
Find orders with at least one high-value item that's in stock:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
any(items, @.price > 100 and @.inStock = true) and status != "cancelled"
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Filter files by extension and location:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
ext(path) = "log" and startsWith(dirname(path), "/var/log")
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Match versioned config files:
|
|
293
|
+
|
|
294
|
+
```
|
|
295
|
+
glob(filename, "config-v*.json") and dirname(path) = "/etc/myapp"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Conditional pricing logic
|
|
299
|
+
|
|
300
|
+
Apply tiered discount based on quantity:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
price * quantity * if(quantity > 100, 0.8, if(quantity > 50, 0.9, 1.0)) < budget
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Working with nested data
|
|
307
|
+
|
|
308
|
+
Check that all addresses in a user's profile have a valid zip code:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
all(user.addresses, regex(@.zipCode, "^\\d{5}(-\\d{4})?$"))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Find users who have made a purchase in a specific category:
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
any(purchases, @.category = "electronics" and @.amount > 500)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Null-safe operations
|
|
321
|
+
|
|
322
|
+
Handle potentially missing nested fields:
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
(user?.profile?.email ?? user?.email ?? "unknown") != "unknown"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Check optional array:
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
length(tags ?? []) > 0
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## AST Format
|
|
335
|
+
|
|
336
|
+
The parsed expression is represented as a JSON-compatible tree:
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
// Input: 'color = "red" and price < 100'
|
|
340
|
+
// AST:
|
|
341
|
+
['and',
|
|
342
|
+
['eq', ['field', 'color'], ['literal', 'red']],
|
|
343
|
+
['lt', ['field', 'price'], ['literal', 100]]
|
|
344
|
+
]
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Node Types
|
|
348
|
+
|
|
349
|
+
| Node | Format |
|
|
350
|
+
|------|--------|
|
|
351
|
+
| Literal | `['literal', value]` |
|
|
352
|
+
| Field access | `['field', 'name']` or `['field', 'parent', 'child']` |
|
|
353
|
+
| Comparison | `['eq' \| 'ne' \| 'lt' \| 'le' \| 'gt' \| 'ge', left, right]` |
|
|
354
|
+
| Logical | `['and' \| 'or', ...children]` or `['not', child]` |
|
|
355
|
+
| Arithmetic | `['add' \| 'sub' \| 'mul' \| 'div' \| 'mod', left, right]` |
|
|
356
|
+
| Function call | `['call', 'funcName', ...args]` |
|
|
357
|
+
| Array index | `['index', array, indexExpr]` |
|
|
358
|
+
| Conditional | `['if', condition, thenExpr, elseExpr]` |
|
|
359
|
+
| Null coalesce | `['coalesce', left, right]` |
|
|
360
|
+
| Optional chain | `['optfield', base, 'fieldName']` |
|
|
361
|
+
|
|
362
|
+
## JavaScript API
|
|
363
|
+
|
|
364
|
+
### `jilt.parse(expression: string): Expression`
|
|
365
|
+
|
|
366
|
+
Parse an expression string into an Expression object.
|
|
367
|
+
|
|
368
|
+
```js
|
|
369
|
+
const expr = jilt.parse('status = "active"')
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `Expression.tree(): Array`
|
|
373
|
+
|
|
374
|
+
Get the AST representation.
|
|
375
|
+
|
|
376
|
+
```js
|
|
377
|
+
expr.tree()
|
|
378
|
+
// => ['eq', ['field', 'status'], ['literal', 'active']]
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### `Expression.compile(): (obj: any) => boolean`
|
|
382
|
+
|
|
383
|
+
Compile to an executable function.
|
|
384
|
+
|
|
385
|
+
```js
|
|
386
|
+
const match = expr.compile()
|
|
387
|
+
match({status: 'active'}) // => true
|
|
388
|
+
match({status: 'deleted'}) // => false
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### `Expression.toString(): string`
|
|
392
|
+
|
|
393
|
+
Convert back to expression string (normalized form).
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
jilt.parse('x=1 AND y = 2').toString()
|
|
397
|
+
// => 'x = 1 and y = 2'
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### `jilt.evaluate(expression: string, obj: any): boolean`
|
|
401
|
+
|
|
402
|
+
Parse, compile, and evaluate in one step.
|
|
403
|
+
|
|
404
|
+
```js
|
|
405
|
+
jilt.evaluate('price < 100', {price: 50})
|
|
406
|
+
// => true
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### `jilt.evaluateConstant(node: ExprNode, options?): unknown`
|
|
410
|
+
|
|
411
|
+
Evaluate an AST node as a constant expression (no input object needed).
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
const expr = jilt.parse('2 + 3 * 4')
|
|
415
|
+
jilt.evaluateConstant(expr.tree())
|
|
416
|
+
// => 14
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Error Handling
|
|
420
|
+
|
|
421
|
+
Parse errors include position information:
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
try {
|
|
425
|
+
jilt.parse('name = ')
|
|
426
|
+
} catch (e) {
|
|
427
|
+
console.log(e.message)
|
|
428
|
+
// => "Unexpected end of expression at position 7"
|
|
429
|
+
console.log(e.position)
|
|
430
|
+
// => 7
|
|
431
|
+
}
|
|
432
|
+
```
|
package/dist/ast.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export type LiteralValue = string | number | boolean | null | unknown[];
|
|
2
|
+
export type LiteralNode = {
|
|
3
|
+
type: 'literal';
|
|
4
|
+
value: LiteralValue;
|
|
5
|
+
};
|
|
6
|
+
export type FieldNode = {
|
|
7
|
+
type: 'field';
|
|
8
|
+
base: ExprNode | null;
|
|
9
|
+
parts: string[];
|
|
10
|
+
};
|
|
11
|
+
export type OptFieldNode = {
|
|
12
|
+
type: 'optfield';
|
|
13
|
+
base: ExprNode;
|
|
14
|
+
field: string;
|
|
15
|
+
};
|
|
16
|
+
export type IndexNode = {
|
|
17
|
+
type: 'index';
|
|
18
|
+
base: ExprNode;
|
|
19
|
+
index: ExprNode;
|
|
20
|
+
};
|
|
21
|
+
export type CallNode = {
|
|
22
|
+
type: 'call';
|
|
23
|
+
name: string;
|
|
24
|
+
args: ExprNode[];
|
|
25
|
+
};
|
|
26
|
+
export type CompareOp = 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge';
|
|
27
|
+
export type CompareNode = {
|
|
28
|
+
type: 'compare';
|
|
29
|
+
op: CompareOp;
|
|
30
|
+
left: ExprNode;
|
|
31
|
+
right: ExprNode;
|
|
32
|
+
};
|
|
33
|
+
export type LogicalNode = {
|
|
34
|
+
type: 'and' | 'or';
|
|
35
|
+
left: ExprNode;
|
|
36
|
+
right: ExprNode;
|
|
37
|
+
};
|
|
38
|
+
export type UnaryNode = {
|
|
39
|
+
type: 'not' | 'neg';
|
|
40
|
+
expr: ExprNode;
|
|
41
|
+
};
|
|
42
|
+
export type BinaryOp = 'add' | 'sub' | 'mul' | 'div' | 'mod';
|
|
43
|
+
export type BinaryNode = {
|
|
44
|
+
type: 'binary';
|
|
45
|
+
op: BinaryOp;
|
|
46
|
+
left: ExprNode;
|
|
47
|
+
right: ExprNode;
|
|
48
|
+
};
|
|
49
|
+
export type IfNode = {
|
|
50
|
+
type: 'if';
|
|
51
|
+
cond: ExprNode;
|
|
52
|
+
thenExpr: ExprNode;
|
|
53
|
+
elseExpr: ExprNode;
|
|
54
|
+
};
|
|
55
|
+
export type CoalesceNode = {
|
|
56
|
+
type: 'coalesce';
|
|
57
|
+
left: ExprNode;
|
|
58
|
+
right: ExprNode;
|
|
59
|
+
};
|
|
60
|
+
export type ExprNode = LiteralNode | FieldNode | OptFieldNode | IndexNode | CallNode | CompareNode | LogicalNode | UnaryNode | BinaryNode | IfNode | CoalesceNode;
|
|
61
|
+
export type TreeNode = ['literal', LiteralValue] | ['field', ...string[]] | ['field', TreeNode, ...string[]] | ['optfield', TreeNode, string] | ['index', TreeNode, TreeNode] | ['call', string, ...TreeNode[]] | ['eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge', TreeNode, TreeNode] | ['and' | 'or', ...TreeNode[]] | ['not', TreeNode] | ['add' | 'sub' | 'mul' | 'div' | 'mod', TreeNode, TreeNode] | ['if', TreeNode, TreeNode, TreeNode] | ['coalesce', TreeNode, TreeNode];
|
|
62
|
+
//# sourceMappingURL=ast.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":"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"}
|
package/dist/ast.js
ADDED
package/dist/ast.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ast.js","sourceRoot":"","sources":["../src/ast.ts"],"names":[],"mappings":""}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parse } from './index.js';
|
|
3
|
+
import { getHelp } from './help.js';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
function printUsage() {
|
|
6
|
+
console.log(`jilt - JSON object filtering\n\nUsage:\n jilt help [topic]\n jilt [--tree] '<expr>'\n jilt -i '<expr>'\n\nExamples:\n echo '{"color":"red"}' | jilt 'color = "red"'\n jilt --tree 'color = "red" and weight < 5'\n jilt -i 'price < 100 and inStock = true'\n`);
|
|
7
|
+
}
|
|
8
|
+
async function main() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const helpFlag = args.includes('--help') || args.includes('-h');
|
|
11
|
+
const helpCommand = args[0] === 'help';
|
|
12
|
+
if (helpFlag || helpCommand) {
|
|
13
|
+
const topic = helpCommand
|
|
14
|
+
? args[1]
|
|
15
|
+
: args.find((arg) => arg !== '--help' && arg !== '-h');
|
|
16
|
+
console.log(getHelp(topic));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const hasTree = args.includes('--tree');
|
|
20
|
+
const interactive = args.includes('-i');
|
|
21
|
+
const exprArg = args.filter((arg) => arg !== '--tree' && arg !== '-i')[0];
|
|
22
|
+
if (!exprArg) {
|
|
23
|
+
printUsage();
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
const expression = parse(exprArg);
|
|
27
|
+
if (hasTree) {
|
|
28
|
+
console.log(JSON.stringify(expression.tree()));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (interactive) {
|
|
32
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
33
|
+
rl.on('line', (line) => {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (!trimmed)
|
|
36
|
+
return;
|
|
37
|
+
try {
|
|
38
|
+
const obj = JSON.parse(trimmed);
|
|
39
|
+
const result = expression.compile()(obj);
|
|
40
|
+
console.log(String(result));
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
44
|
+
console.error(message);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const input = await readStdin();
|
|
50
|
+
if (!input.trim()) {
|
|
51
|
+
throw new Error('Expected JSON input on stdin');
|
|
52
|
+
}
|
|
53
|
+
const obj = JSON.parse(input);
|
|
54
|
+
const result = expression.compile()(obj);
|
|
55
|
+
console.log(String(result));
|
|
56
|
+
}
|
|
57
|
+
main().catch((err) => {
|
|
58
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59
|
+
console.error(message);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
async function readStdin() {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
for await (const chunk of process.stdin) {
|
|
65
|
+
chunks.push(Buffer.from(chunk));
|
|
66
|
+
}
|
|
67
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExprNode, TreeNode } from './ast.js';
|
|
2
|
+
export type Strictness = boolean | null | undefined;
|
|
3
|
+
export declare class EvaluationError extends Error {
|
|
4
|
+
code?: 'missing' | 'type' | 'arity';
|
|
5
|
+
constructor(message: string, code?: 'missing' | 'type' | 'arity');
|
|
6
|
+
}
|
|
7
|
+
export type EvalOptions = {
|
|
8
|
+
strict?: Strictness;
|
|
9
|
+
allowFields?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function normalizeStrict(strict: Strictness): boolean;
|
|
12
|
+
export declare function compile(node: ExprNode, options?: EvalOptions): (obj: unknown) => boolean;
|
|
13
|
+
export declare function evaluate(node: ExprNode, obj: unknown, options?: EvalOptions): boolean;
|
|
14
|
+
export declare function evaluateConstant(node: ExprNode, options?: EvalOptions): unknown;
|
|
15
|
+
export declare function toTree(node: ExprNode): TreeNode;
|
|
16
|
+
export type { TreeNode };
|
|
17
|
+
//# sourceMappingURL=compiler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiler.d.ts","sourceRoot":"","sources":["../src/compiler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAMV,QAAQ,EAOR,QAAQ,EAET,MAAM,UAAU,CAAA;AAEjB,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAEnD,qBAAa,eAAgB,SAAQ,KAAK;IACxC,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAA;gBAEvB,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO;CAKjE;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AASD,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAG3D;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAE,WAAgB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAO5F;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAEzF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAInF;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CA8B/C;AAisBD,YAAY,EAAE,QAAQ,EAAE,CAAA"}
|