json-logic-rb 0.1.0.beta1
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +315 -0
- data/lib/json_logic/engine.rb +54 -0
- data/lib/json_logic/enumerable_operation.rb +9 -0
- data/lib/json_logic/lazy_operation.rb +3 -0
- data/lib/json_logic/operation.rb +14 -0
- data/lib/json_logic/operations/add.rb +6 -0
- data/lib/json_logic/operations/all.rb +16 -0
- data/lib/json_logic/operations/and.rb +14 -0
- data/lib/json_logic/operations/bool_cast.rb +9 -0
- data/lib/json_logic/operations/cat.rb +6 -0
- data/lib/json_logic/operations/div.rb +6 -0
- data/lib/json_logic/operations/equal.rb +6 -0
- data/lib/json_logic/operations/filter.rb +15 -0
- data/lib/json_logic/operations/gt.rb +6 -0
- data/lib/json_logic/operations/gte.rb +6 -0
- data/lib/json_logic/operations/if.rb +15 -0
- data/lib/json_logic/operations/in_op.rb +6 -0
- data/lib/json_logic/operations/lt.rb +10 -0
- data/lib/json_logic/operations/lte.rb +10 -0
- data/lib/json_logic/operations/map.rb +10 -0
- data/lib/json_logic/operations/max.rb +6 -0
- data/lib/json_logic/operations/merge.rb +8 -0
- data/lib/json_logic/operations/min.rb +6 -0
- data/lib/json_logic/operations/missing.rb +33 -0
- data/lib/json_logic/operations/missing_some.rb +27 -0
- data/lib/json_logic/operations/mod.rb +6 -0
- data/lib/json_logic/operations/mul.rb +6 -0
- data/lib/json_logic/operations/none.rb +14 -0
- data/lib/json_logic/operations/not.rb +6 -0
- data/lib/json_logic/operations/not_equal.rb +6 -0
- data/lib/json_logic/operations/or.rb +12 -0
- data/lib/json_logic/operations/reduce.rb +19 -0
- data/lib/json_logic/operations/some.rb +16 -0
- data/lib/json_logic/operations/strict_equal.rb +6 -0
- data/lib/json_logic/operations/strict_not_equal.rb +6 -0
- data/lib/json_logic/operations/sub.rb +6 -0
- data/lib/json_logic/operations/substr.rb +30 -0
- data/lib/json_logic/operations/ternary.rb +13 -0
- data/lib/json_logic/operations/var.rb +27 -0
- data/lib/json_logic/registry.rb +19 -0
- data/lib/json_logic/semantics.rb +41 -0
- data/lib/json_logic/version.rb +3 -0
- data/lib/json_logic.rb +42 -0
- data/script/compliance.rb +50 -0
- data/spec/tmp/tests.js +0 -0
- data/spec/tmp/tests.json +532 -0
- data/test/selftest.rb +15 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3f6841b0bb7968ba96acb42d78d167b14c9757b66f7e76a87b474a6af22958e5
|
|
4
|
+
data.tar.gz: 1c6a5520ce8de25ad8191542e2e0aeb3ce57846ef40d8bcd66ed0a061a5ed407
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6c988634418f03c386e0c53ccd5323b2566bf13679d81b15c7f22538b7179b3d6a49f9998520d5155fd36e249d0b809951cc164a11d0ae425a9e09e67cf56b2d
|
|
7
|
+
data.tar.gz: 5e8e604fd9cc6cd1c927d071478db8cc4c00d6ca5bced126fba001d481998df551c766312d91aed49800495633220df85391cad88b0e6e98c235c8a9e0dc13c3
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# json-logic-rb
|
|
2
|
+
|
|
3
|
+
Ruby implementation of [JsonLogic](https://jsonlogic.com/) — simple and extensible. Ships with a compliance runner for the official test suite.
|
|
4
|
+
|
|
5
|
+
<a href="#"><img alt="build" src="https://img.shields.io/github/actions/workflow/status/your-org/json-logic-rb/ci.yml?branch=main"> <a href="https://rubygems.org/gems/json-logic-rb"><img alt="rubygems" src="https://img.shields.io/gem/v/json-logic-rb"></a> <a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-informational"></a>
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
- [What](#what)
|
|
10
|
+
- [Install](#install)
|
|
11
|
+
- [Quick start](#quick-start)
|
|
12
|
+
- [How](#how)
|
|
13
|
+
- [1) Operators (default)](#1-operators-default)
|
|
14
|
+
- [2) Lazy operators](#2-lazy--operators)
|
|
15
|
+
- [Supported Built-in Operations](#supported-built-in-operations)
|
|
16
|
+
- [Accessing Data](#accessing-data)
|
|
17
|
+
- [Logic and Boolean Operations](#logic-and-boolean-operations)
|
|
18
|
+
- [Numeric Operations](#numeric-operations)
|
|
19
|
+
- [Array Operations](#array-operations)
|
|
20
|
+
- [String Operations](#string-operations)
|
|
21
|
+
- [Miscellaneous](#miscellaneous)
|
|
22
|
+
- [Extending (add your own operator)](#extending-add-your-own-operator)
|
|
23
|
+
- [Public Interface](#public-interface)
|
|
24
|
+
- [Compliance & tests](#compliance--tests)
|
|
25
|
+
- [Security](#security)
|
|
26
|
+
- [License](#license)
|
|
27
|
+
- [Contributing](#contributing)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## What
|
|
32
|
+
JsonLogic rules are JSON trees. The engine walks that tree and returns a Ruby value.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
gem install json-logic-rb
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require "json-logic-rb"
|
|
47
|
+
|
|
48
|
+
rule = { "+" => [1, 2, 3] }
|
|
49
|
+
|
|
50
|
+
JsonLogic.apply(rule)
|
|
51
|
+
# => 6.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
With data:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
JsonLogic.apply({ "var" => "user.age" }, { "user" => { "age" => 42 } })
|
|
58
|
+
# => 42
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## How
|
|
63
|
+
|
|
64
|
+
There are **two kinds of operators** in this implementation. This mapping follows the official behavior described on [jsonlogic.com](https://jsonlogic.com).
|
|
65
|
+
|
|
66
|
+
### 1) Operators (default)
|
|
67
|
+
|
|
68
|
+
For **operators**, the engine **evaluates all arguments first** and then calls the operator with the **resulting Ruby values**.
|
|
69
|
+
This matches the reference behavior for arithmetic, comparisons, string operations, and other pure operators that do not control evaluation order.
|
|
70
|
+
|
|
71
|
+
**Official docs:**
|
|
72
|
+
|
|
73
|
+
- Numeric Operations — [https://jsonlogic.com/operations.html#numeric-operations](https://jsonlogic.com/operations.html#numeric-operations)
|
|
74
|
+
|
|
75
|
+
- String Operations — [https://jsonlogic.com/operations.html#string-operations](https://jsonlogic.com/operations.html#string-operations)
|
|
76
|
+
|
|
77
|
+
- Array Operations (simple transforms like `merge`, membership `in`) — [https://jsonlogic.com/operations.html#array-operations](https://jsonlogic.com/operations.html#array-operations)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
### 2) Lazy operators
|
|
83
|
+
|
|
84
|
+
Some operators must control **whether** and **when** their arguments are evaluated.
|
|
85
|
+
They implement branching, short-circuiting, or “apply a rule per item” semantics.
|
|
86
|
+
For these **lazy operators**, the engine passes **raw sub-rules** and current `data`.
|
|
87
|
+
The operator then evaluates only the sub-rules it actually needs.
|
|
88
|
+
|
|
89
|
+
**Groups and references:**
|
|
90
|
+
|
|
91
|
+
- **Branching / boolean control** — `if`, `?:`, `and`, `or`, `var`
|
|
92
|
+
Docs: Logic and Boolean Operations — [https://jsonlogic.com/operations.html#logic-and-boolean-operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations)
|
|
93
|
+
Truthiness table used by these operators — [https://jsonlogic.com/truthy.html](https://jsonlogic.com/truthy.html)
|
|
94
|
+
|
|
95
|
+
- **Enumerable operators** — `map`, `filter`, `reduce`, `all`, `none`, `some`
|
|
96
|
+
Docs: Array Operations — [https://jsonlogic.com/operations.html#array-operations](https://jsonlogic.com/operations.html#array-operations)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
**How per-item evaluation works:**
|
|
100
|
+
|
|
101
|
+
1. The first argument is a rule that returns the list of items — evaluated **once** to a Ruby array.
|
|
102
|
+
|
|
103
|
+
2. The second argument is the per-item rule — evaluated **for each item** with that item as the **current root**.
|
|
104
|
+
|
|
105
|
+
3. For `reduce`, the current item is also available as `"current"`, and the running total as `"accumulator"` (this mirrors the reference usage in docs and tests).
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
**Examples**
|
|
109
|
+
```ruby
|
|
110
|
+
`# filter: keep numbers >= 2
|
|
111
|
+
JsonLogic.apply(
|
|
112
|
+
{ "filter" => [ { "var" => "ints" }, { ">=" => [ { "var" => "" }, 2 ] } ] },
|
|
113
|
+
{ "ints" => [1,2,3] }
|
|
114
|
+
)
|
|
115
|
+
# => [2, 3]
|
|
116
|
+
|
|
117
|
+
# reduce: sum using "current" and "accumulator"
|
|
118
|
+
JsonLogic.apply(
|
|
119
|
+
{ "reduce" => [
|
|
120
|
+
{ "var" => "ints" },
|
|
121
|
+
{ "+" => [ { "var" => "accumulator" }, { "var" => "current" } ] }, 0 ]
|
|
122
|
+
},
|
|
123
|
+
{ "ints" => [1,2,3,4] }
|
|
124
|
+
)
|
|
125
|
+
# => 10.0
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Why laziness matters?
|
|
129
|
+
|
|
130
|
+
Lazy operators **prevent evaluation** of branches you do not need.
|
|
131
|
+
If division by zero raised an error (hypothetically), lazy control would avoid it:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# "or" short-circuits: 1 is truthy, so the right side is NOT evaluated.
|
|
135
|
+
# If the right side were evaluated eagerly, it would attempt 1/0 (error).
|
|
136
|
+
JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
|
|
137
|
+
# => 1
|
|
138
|
+
|
|
139
|
+
# "if" evaluates only the 'then' branch when condition is true.
|
|
140
|
+
# The 'else' branch with 1/0 is never evaluated.
|
|
141
|
+
JsonLogic.apply({ "if" => [true, 42, { "/" => [1, 0] }] })
|
|
142
|
+
# => 42
|
|
143
|
+
```
|
|
144
|
+
>In this gem (library) `/` returns `nil` on divide-by-zero, but these examples show **why** lazy evaluation is required by the spec: branching and boolean operators must **not** evaluate unused branches.
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Supported Built-in Operations
|
|
151
|
+
|
|
152
|
+
Below is a checklist that mirrors the sections on [**jsonlogic.com/operations.html**](https://jsonlogic.com/operations.html) and shows what this gem (library) implements.
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
### Accessing Data
|
|
156
|
+
| Operator | Supported | Notes |
|
|
157
|
+
|---|---:|---|
|
|
158
|
+
| `var` | ✅ | |
|
|
159
|
+
| `missing` | ✅ | Returns array of missing keys; works with complex sources (e.g. `merge` result). |
|
|
160
|
+
| `missing_some` | ✅ | Returns `[]` when the minimum is met, otherwise missing keys. |
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
### [Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations])
|
|
164
|
+
| Operator | Supported | Notes |
|
|
165
|
+
|---|---:|---|
|
|
166
|
+
| `if` | ✅ | |
|
|
167
|
+
| `==` | ✅ | |
|
|
168
|
+
| `===` | ✅ | Strict equality (same type and value). |
|
|
169
|
+
| `!=` | ✅ | |
|
|
170
|
+
| `!==` | ✅ | |
|
|
171
|
+
| `!` | ✅ | Follows JsonLogic truthiness. |
|
|
172
|
+
| `!!` | ✅ | Follows JsonLogic truthiness. |
|
|
173
|
+
| `or` | ✅ | Returns first truthy / last value; |
|
|
174
|
+
| `and` | ✅ | Returns first falsy / last value; |
|
|
175
|
+
| `?:` | ✅ | Returns the truth. |
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### [Numeric Operations](https://jsonlogic.com/operations.html#numeric-operations)
|
|
179
|
+
| Operator / Topic | Supported | Notes |
|
|
180
|
+
|---|---:|---|
|
|
181
|
+
| `>` `>=` `<` `<=` | ✅ | |
|
|
182
|
+
| Between (pattern) | ✅ | Use `<`/`<=` with 3 args (not a separate op). |
|
|
183
|
+
| `max` / `min` | ✅ | |
|
|
184
|
+
| `+` `-` `*` `/` | ✅ | Unary `-` negates; unary `+` casts to number; `/` returns `nil` on divide‑by‑zero. |
|
|
185
|
+
| `%` | ✅ | |
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
### [Array Operations](https://jsonlogic.com/operations.html#array-operations)
|
|
189
|
+
| Operator | Supported | Notes |
|
|
190
|
+
|---|---:|---|
|
|
191
|
+
| `map` | ✅ | |
|
|
192
|
+
| `reduce` | ✅ | Per‑item rule sees `"current"` and `"accumulator"`. |
|
|
193
|
+
| `filter` | ✅ | Keeps items where per‑item rule is truthy (follows JsonLogic truthiness). |
|
|
194
|
+
| `all` | ✅ | `false` on empty; all per‑item are truthy (follows JsonLogic truthiness). |
|
|
195
|
+
| `none` | ✅ | `true` on empty; none per‑item are truthy (follows JsonLogic truthiness). |
|
|
196
|
+
| `some` | ✅ | `false` on empty; any per‑item is truthy (follows JsonLogic truthiness). |
|
|
197
|
+
| `merge` | ✅ | Flattens arguments into a single array; non‑arrays cast to singleton arrays. |
|
|
198
|
+
| `in` | ✅ | If second arg is an array: membership test. |
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
### [String Operations](https://jsonlogic.com/operations.html#string-operations)
|
|
202
|
+
| Operator | Supported | Notes |
|
|
203
|
+
|---|---:|---|
|
|
204
|
+
| `in` | ✅ | If second arg is a string: substring test. |
|
|
205
|
+
| `cat` | ✅ | Concatenate arguments as strings (no delimiter). |
|
|
206
|
+
| `substr` | ✅ | Start index can be negative; length can be omitted or negative (mirrors the official behavior). |
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
### Miscellaneous
|
|
210
|
+
| Operator | Supported | Notes |
|
|
211
|
+
|---|---:|---|
|
|
212
|
+
| `log` | 🚫 | Not implemented by default (and not part of the compliance tests). |
|
|
213
|
+
|
|
214
|
+
**Summary:** From the reference page’s list, everything except `log` is implemented.
|
|
215
|
+
(“Between” is not a standalone operator, but the `<`/`<=` 3‑argument form is supported.)
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Extending (add your own operator)
|
|
220
|
+
|
|
221
|
+
### Operation Type
|
|
222
|
+
|
|
223
|
+
Each operator is a class.
|
|
224
|
+
- **Value operations** inherit `JsonLogic::Operation` (engine passes values).
|
|
225
|
+
|
|
226
|
+
- **Lazy operations** inherit `JsonLogic::LazyOperation` (engine passes raw sub‑rules).
|
|
227
|
+
|
|
228
|
+
- **Enumerable operations** inherit `JsonLogic::EnumerableOperation` (standardized data binding for per‑item rules).
|
|
229
|
+
|
|
230
|
+
### Guide
|
|
231
|
+
|
|
232
|
+
First, create the Class for you Operation based on it's type:
|
|
233
|
+
```ruby
|
|
234
|
+
class JsonLogic::Operations::StartsWith < JsonLogic::Operation
|
|
235
|
+
def self.op_name = "starts_with" # {"starts_with": [string, prefix]}
|
|
236
|
+
|
|
237
|
+
def call((str, prefix), _data)
|
|
238
|
+
str.to_s.start_with?(prefix.to_s)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Second, register your operation:
|
|
244
|
+
```ruby
|
|
245
|
+
JsonLogic::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Use it!
|
|
249
|
+
```ruby
|
|
250
|
+
rule = {
|
|
251
|
+
"if" => [
|
|
252
|
+
{ "starts_with" => [ { "var" => "email" }, "admin@" ] },
|
|
253
|
+
"is_admin",
|
|
254
|
+
"regular_user"
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
p JsonLogic.apply(rule, { "email" => "admin@example.com" })
|
|
259
|
+
# => "is_admin"
|
|
260
|
+
p JsonLogic.apply(rule, { "email" => "user@example.com" })
|
|
261
|
+
# => "regular_user"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Public Interface
|
|
267
|
+
Use the high-level facade to evaluate a JsonLogic rule against input and get a plain Ruby value back.
|
|
268
|
+
If you need more control (e.g., custom operator sets or multiple engines), use `JsonLogic::Engine`
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
# Main facade
|
|
273
|
+
JsonLogic.apply(rule, data = nil) # => value
|
|
274
|
+
|
|
275
|
+
# Engine/Registry (advanced)
|
|
276
|
+
engine = JsonLogic::Engine.default
|
|
277
|
+
engine.evaluate(rule, data)
|
|
278
|
+
|
|
279
|
+
# Register a custom op class (auto‑registration is also supported)
|
|
280
|
+
engine.registry.register(JsonLogic::Operations::StartsWith)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Compliance & tests
|
|
286
|
+
Optional: quick self-test
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
ruby test/selftest.rb
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Official test suite
|
|
293
|
+
1. Fetch the official suite
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
mkdir -p spec/tmp
|
|
297
|
+
curl -fsSL https://jsonlogic.com/tests.json -o spec/tmp/tests.json
|
|
298
|
+
```
|
|
299
|
+
2. Run it
|
|
300
|
+
```bash
|
|
301
|
+
ruby script/compliance.rb spec/tmp/tests.json
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Expected output
|
|
305
|
+
```bash
|
|
306
|
+
# => Compliance: X/X passed
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Security
|
|
312
|
+
|
|
313
|
+
- Rules are **data**, not code; no Ruby eval.
|
|
314
|
+
- When evaluating untrusted rules, consider adding a timeout and error handling at the call site.
|
|
315
|
+
- Custom operations should be **pure** (no IO, no network, no shell).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'semantics'
|
|
4
|
+
|
|
5
|
+
module JsonLogic
|
|
6
|
+
class Engine
|
|
7
|
+
include Semantics
|
|
8
|
+
|
|
9
|
+
def self.default
|
|
10
|
+
@default ||= new(registry: Registry.new)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(registry:)
|
|
14
|
+
@registry = registry
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :registry
|
|
18
|
+
|
|
19
|
+
def evaluate(rule, data = nil)
|
|
20
|
+
data ||= {}
|
|
21
|
+
|
|
22
|
+
case rule
|
|
23
|
+
when Numeric,
|
|
24
|
+
String,
|
|
25
|
+
TrueClass,
|
|
26
|
+
FalseClass,
|
|
27
|
+
NilClass
|
|
28
|
+
rule
|
|
29
|
+
when Array
|
|
30
|
+
rule.map { |r| evaluate(r, data) }
|
|
31
|
+
when Hash
|
|
32
|
+
name, raw_args = rule.first
|
|
33
|
+
op_class = @registry.fetch(name)
|
|
34
|
+
raise ArgumentError, "unknown operation: #{name}" unless op_class
|
|
35
|
+
|
|
36
|
+
args =
|
|
37
|
+
case raw_args
|
|
38
|
+
when nil then []
|
|
39
|
+
when Array then raw_args
|
|
40
|
+
else [raw_args]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if op_class.values_only?
|
|
44
|
+
values = args.map { |a| evaluate(a, data) }
|
|
45
|
+
op_class.new.call(values, data)
|
|
46
|
+
else
|
|
47
|
+
op_class.new.call(args, data)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
rule
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
class JsonLogic::EnumerableOperation < JsonLogic::LazyOperation
|
|
2
|
+
private
|
|
3
|
+
|
|
4
|
+
def resolve_items_and_per_item_rule(rules, data)
|
|
5
|
+
rule_that_returns_items, rule_applied_to_each_item = rules
|
|
6
|
+
items = Array(JsonLogic.apply(rule_that_returns_items, data))
|
|
7
|
+
[items, rule_applied_to_each_item]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::All < JsonLogic::EnumerableOperation
|
|
4
|
+
def self.op_name = "all"
|
|
5
|
+
|
|
6
|
+
def call(args, data)
|
|
7
|
+
items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
|
|
8
|
+
return false if items.empty?
|
|
9
|
+
|
|
10
|
+
items.all? do |item|
|
|
11
|
+
JsonLogic::Semantics.truthy?(
|
|
12
|
+
JsonLogic.apply(rule_applied_to_each_item, item)
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::And < JsonLogic::LazyOperation
|
|
4
|
+
def self.op_name = "and"
|
|
5
|
+
|
|
6
|
+
def call(args, data)
|
|
7
|
+
last = nil
|
|
8
|
+
args.each do |a|
|
|
9
|
+
last = JsonLogic.apply(a, data)
|
|
10
|
+
return last unless JsonLogic::Semantics.truthy?(last)
|
|
11
|
+
end
|
|
12
|
+
last
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::Filter < JsonLogic::EnumerableOperation
|
|
4
|
+
def self.op_name = "filter"
|
|
5
|
+
|
|
6
|
+
def call(args, data)
|
|
7
|
+
items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
|
|
8
|
+
|
|
9
|
+
items.filter do |item|
|
|
10
|
+
JsonLogic::Semantics.truthy?(
|
|
11
|
+
JsonLogic.apply(rule_applied_to_each_item, item)
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::If < JsonLogic::LazyOperation
|
|
4
|
+
def self.op_name = "if"
|
|
5
|
+
|
|
6
|
+
def call(args, data)
|
|
7
|
+
i = 0
|
|
8
|
+
while i < args.size - 1
|
|
9
|
+
return JsonLogic.apply(args[i + 1], data) if JsonLogic::Semantics.truthy?(JsonLogic.apply(args[i], data))
|
|
10
|
+
i += 2
|
|
11
|
+
end
|
|
12
|
+
return JsonLogic.apply(args[-1], data) if args.size.odd?
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::LT < JsonLogic::Operation
|
|
4
|
+
def self.op_name = "<"
|
|
5
|
+
def call(values, _data)
|
|
6
|
+
nums = values.map { |v| JsonLogic::Semantics.to_number(v) }
|
|
7
|
+
return nums[0] < nums[1] if nums.size == 2
|
|
8
|
+
nums.each_cons(2).all? { |a,b| a < b }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::LTE < JsonLogic::Operation
|
|
4
|
+
def self.op_name = "<="
|
|
5
|
+
def call(values, _data)
|
|
6
|
+
nums = values.map { |v| JsonLogic::Semantics.to_number(v) }
|
|
7
|
+
return nums[0] <= nums[1] if nums.size == 2
|
|
8
|
+
nums.each_cons(2).all? { |a,b| a <= b }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::Map < JsonLogic::EnumerableOperation
|
|
4
|
+
def self.op_name = "map"
|
|
5
|
+
|
|
6
|
+
def call(args, data)
|
|
7
|
+
items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
|
|
8
|
+
items.map { |item| JsonLogic.apply(rule_applied_to_each_item, item) }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class JsonLogic::Operations::Missing < JsonLogic::Operation
|
|
4
|
+
def self.op_name = "missing"
|
|
5
|
+
|
|
6
|
+
def call(values, data)
|
|
7
|
+
keys =
|
|
8
|
+
if values.size == 1 && values.first.is_a?(Array)
|
|
9
|
+
values.first
|
|
10
|
+
else
|
|
11
|
+
values
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
keys.select { |k| dig(data, k).nil? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def dig(obj, path)
|
|
20
|
+
return nil if obj.nil?
|
|
21
|
+
cur = obj
|
|
22
|
+
path.to_s.split(".").each do |k|
|
|
23
|
+
if cur.is_a?(Array) && k =~ /\A\d+\z/
|
|
24
|
+
cur = cur[k.to_i]
|
|
25
|
+
elsif cur.is_a?(Hash)
|
|
26
|
+
cur = cur[k] || cur[k.to_s] || cur[k.to_sym]
|
|
27
|
+
else
|
|
28
|
+
return nil
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
cur
|
|
32
|
+
end
|
|
33
|
+
end
|