json-logic-rb 0.1.1.beta2 → 0.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.
- checksums.yaml +4 -4
- data/README.md +212 -169
- data/lib/json_logic/operations/all.rb +3 -1
- data/lib/json_logic/operations/and.rb +3 -1
- data/lib/json_logic/operations/bool_cast.rb +2 -1
- data/lib/json_logic/operations/filter.rb +3 -1
- data/lib/json_logic/operations/if.rb +3 -1
- data/lib/json_logic/operations/none.rb +3 -1
- data/lib/json_logic/operations/not.rb +3 -1
- data/lib/json_logic/operations/or.rb +3 -1
- data/lib/json_logic/operations/some.rb +3 -1
- data/lib/json_logic/operations/strict_not_equal.rb +2 -0
- data/lib/json_logic/operations/ternary.rb +3 -1
- data/lib/json_logic/semantics.rb +44 -29
- data/lib/json_logic/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc614e1da0fa7f6a40304c0e988508dc71f67b2f65ded67e3ecdbf5be6b4c2ce
|
|
4
|
+
data.tar.gz: 2e80d861339668970a908e3ffbc6f3ea7562daa51850ce9ce90a1eb35b6ca598
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b28f05aeb82f7163634d49b7a1eec0e3988b68bd6981111f8ee94de43fc000293730740c116dcb3d757500e58c22672ef1b913199b4d1a901fd50c18fee0963f
|
|
7
|
+
data.tar.gz: f724dda9f6f0237144fac1255c9b0b20a848decb6981d6a3c19606375a5611c8367bed214e987ecb88b38c9cdaed1e73478ff17052a055b4b0249dfd1ff76bf4
|
data/README.md
CHANGED
|
@@ -1,39 +1,30 @@
|
|
|
1
|
-
# json-logic-rb
|
|
2
1
|
|
|
3
|
-
|
|
2
|
+
# json-logic-rb
|
|
4
3
|
|
|
5
|
-
|
|
4
|
+
Ruby implementation of [JsonLogic](https://jsonlogic.com/) — simple and extensible. Ships with a compliance runner for the official test suite.
|
|
6
5
|
|
|
6
|
+
<a href="#"><img alt="build" src="https://img.shields.io/github/actions/workflow/status/your-org/json-logic-rb/ci-complience?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>
|
|
7
7
|
|
|
8
8
|
## Table of Contents
|
|
9
9
|
- [What](#what)
|
|
10
10
|
- [Install](#install)
|
|
11
11
|
- [Quick start](#quick-start)
|
|
12
12
|
- [How](#how)
|
|
13
|
-
- [1
|
|
14
|
-
- [2
|
|
15
|
-
- [Supported Built
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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-and-tests)
|
|
13
|
+
- [1. Default Operations](#1-default-operations)
|
|
14
|
+
- [2. Lazy Operations](#2-lazy-operations)
|
|
15
|
+
- [Supported Operations (Built‑in)](#supported-operations-built-in)
|
|
16
|
+
- [Adding Operations](#adding-operations)
|
|
17
|
+
- [JsonLogic Semantic](#jsonlogic-semantic)
|
|
18
|
+
- [Compliance and tests](#compliance-and-tests)
|
|
25
19
|
- [Security](#security)
|
|
26
20
|
- [License](#license)
|
|
27
|
-
- [
|
|
28
|
-
|
|
21
|
+
- [Authors](#authors)
|
|
29
22
|
|
|
23
|
+
---
|
|
30
24
|
|
|
31
25
|
## What
|
|
32
26
|
JsonLogic rules are JSON trees. The engine walks that tree and returns a Ruby value.
|
|
33
27
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
28
|
## Install
|
|
38
29
|
|
|
39
30
|
```bash
|
|
@@ -58,54 +49,42 @@ JsonLogic.apply({ "var" => "user.age" }, { "user" => { "age" => 42 } })
|
|
|
58
49
|
# => 42
|
|
59
50
|
```
|
|
60
51
|
|
|
61
|
-
|
|
62
52
|
## How
|
|
63
53
|
|
|
64
|
-
There are **two
|
|
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.
|
|
54
|
+
There are **two types of operations** in this implementation: Default Operations and Lazy Operations.
|
|
70
55
|
|
|
71
|
-
|
|
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)
|
|
56
|
+
### 1. Default Operations
|
|
78
57
|
|
|
58
|
+
For **Default Operations**, the engine **evaluates all arguments first** and then calls the operator with the **resulting Ruby values**.
|
|
59
|
+
This matches the reference behavior for arithmetic, comparisons, string operations, and other pure operations that do not control evaluation order.
|
|
79
60
|
|
|
61
|
+
**Groups and references:**
|
|
80
62
|
|
|
63
|
+
- [Numeric operations](https://jsonlogic.com/operations.html#numeric-operations)
|
|
64
|
+
- [String operations](https://jsonlogic.com/operations.html#string-operations)
|
|
65
|
+
- [Array operations](https://jsonlogic.com/operations.html#array-operations) — simple transforms like `merge`, membership `in`.
|
|
81
66
|
|
|
82
|
-
### 2
|
|
67
|
+
### 2. Lazy Operations
|
|
83
68
|
|
|
84
|
-
Some
|
|
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.
|
|
69
|
+
Some operations must control **whether** and **when** their arguments are evaluated. They implement branching, short-circuiting, or “apply a rule per item” semantics. For these **Lazy Operations**, the engine passes **raw sub-rules** and current data. The operator then evaluates only the sub-rules it actually needs.
|
|
88
70
|
|
|
89
71
|
**Groups and references:**
|
|
90
72
|
|
|
91
|
-
-
|
|
92
|
-
|
|
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)
|
|
73
|
+
- **Branching / boolean control** — `if`, `?:`, `and`, `or`, `var`
|
|
74
|
+
[Logic & boolean operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) • [Truthiness](https://jsonlogic.com/truthy.html)
|
|
97
75
|
|
|
76
|
+
- **Enumerable operators** — `map`, `filter`, `reduce`, `all`, `none`, `some`
|
|
77
|
+
[Array operations](https://jsonlogic.com/operations.html#array-operations)
|
|
98
78
|
|
|
99
|
-
**How per-item evaluation works:**
|
|
79
|
+
**How enumerable per-item evaluation works:**
|
|
100
80
|
|
|
101
|
-
1.
|
|
81
|
+
1. The first argument is a rule that returns the list of items — evaluated **once** to a Ruby array.
|
|
82
|
+
2. The second argument is the per-item rule — evaluated **for each item** with that item as the **current root**.
|
|
83
|
+
3. For `reduce`, the current item is also available as `"current"`, and the running total as `"accumulator"`.
|
|
102
84
|
|
|
103
|
-
2. The second argument is the per-item rule — evaluated **for each item** with that item as the **current root**.
|
|
104
85
|
|
|
105
|
-
|
|
86
|
+
**Example #1**
|
|
106
87
|
|
|
107
|
-
|
|
108
|
-
**Examples**
|
|
109
88
|
```ruby
|
|
110
89
|
# filter: keep numbers >= 2
|
|
111
90
|
JsonLogic.apply(
|
|
@@ -113,7 +92,11 @@ JsonLogic.apply(
|
|
|
113
92
|
{ "ints" => [1,2,3] }
|
|
114
93
|
)
|
|
115
94
|
# => [2, 3]
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Example #2**
|
|
116
98
|
|
|
99
|
+
```ruby
|
|
117
100
|
# reduce: sum using "current" and "accumulator"
|
|
118
101
|
JsonLogic.apply(
|
|
119
102
|
{ "reduce" => [
|
|
@@ -127,193 +110,253 @@ JsonLogic.apply(
|
|
|
127
110
|
|
|
128
111
|
### Why laziness matters?
|
|
129
112
|
|
|
130
|
-
Lazy
|
|
131
|
-
If division by zero raised an error (hypothetically), lazy control would avoid it:
|
|
113
|
+
Lazy operations **prevent evaluation** of branches you do not need. If division by zero raised an error (hypothetically), lazy control would avoid it:
|
|
132
114
|
|
|
133
115
|
```ruby
|
|
134
116
|
# "or" short-circuits: 1 is truthy, so the right side is NOT evaluated.
|
|
135
117
|
# If the right side were evaluated eagerly, it would attempt 1/0 (error).
|
|
136
118
|
JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
|
|
137
119
|
# => 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
120
|
```
|
|
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
121
|
|
|
122
|
+
> In this gem `/` 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.
|
|
123
|
+
|
|
124
|
+
## Supported Operations (Built‑in)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
Below is a list that mirrors the sections on [jsonlogic.com/operations.html](https://jsonlogic.com/operations.html) and shows what this gem (library) implements. From the reference page’s list, everything except `log` is implemented.
|
|
128
|
+
|
|
129
|
+
| Operator | Supported |
|
|
130
|
+
|---|---:|
|
|
131
|
+
| `var` | ✅ |
|
|
132
|
+
| `missing` | ✅ |
|
|
133
|
+
| `missing_some` | ✅ |
|
|
134
|
+
|[Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations])
|
|
135
|
+
| `if` | ✅ |
|
|
136
|
+
| `==` | ✅ |
|
|
137
|
+
| `===` | ✅ |
|
|
138
|
+
| `!=` | ✅ |
|
|
139
|
+
| `!==` | ✅ |
|
|
140
|
+
| `!` | ✅ |
|
|
141
|
+
| `!!` | ✅ |
|
|
142
|
+
| `or` | ✅ |
|
|
143
|
+
| `and` | ✅ |
|
|
144
|
+
| `?:` | ✅ |
|
|
145
|
+
|[Numeric Operations](https://jsonlogic.com/operations.html#numeric-operations)|
|
|
146
|
+
| `map` | ✅ |
|
|
147
|
+
| `reduce` | ✅ |
|
|
148
|
+
| `filter` | ✅ |
|
|
149
|
+
| `all` | ✅ |
|
|
150
|
+
| `none` | ✅ |
|
|
151
|
+
| `some` | ✅ |
|
|
152
|
+
| `merge` | ✅ |
|
|
153
|
+
| `in` | ✅ |
|
|
154
|
+
|[Array Operations](https://jsonlogic.com/operations.html#array-operations)|
|
|
155
|
+
| `map` | ✅ |
|
|
156
|
+
| `reduce` | ✅ |
|
|
157
|
+
| `filter` | ✅ |
|
|
158
|
+
| `all` | ✅ |
|
|
159
|
+
| `none` | ✅ |
|
|
160
|
+
| `some` | ✅ |
|
|
161
|
+
| `merge` | ✅ |
|
|
162
|
+
| `in` | ✅ |
|
|
163
|
+
|[String Operations](https://jsonlogic.com/operations.html#string-operations)|
|
|
164
|
+
| `in` | ✅ |
|
|
165
|
+
| `cat` | ✅ |
|
|
166
|
+
| `substr` | ✅ |
|
|
167
|
+
|Miscellaneous|
|
|
168
|
+
| `log` | 🚫 |
|
|
169
|
+
|
|
170
|
+
## Adding Operations
|
|
171
|
+
|
|
172
|
+
Need a custom operation? It’s straightforward.
|
|
173
|
+
|
|
174
|
+
### 1) Pick the operation type
|
|
175
|
+
Choose one of:
|
|
176
|
+
- **Default**
|
|
177
|
+
```ruby
|
|
178
|
+
class JsonLogic::Operations::StartsWith < JsonLogic::Operation; end
|
|
179
|
+
```
|
|
180
|
+
- **Lazy**
|
|
181
|
+
```ruby
|
|
182
|
+
class JsonLogic::Operations::StartsWith < JsonLogic::LazyOperation; end
|
|
183
|
+
```
|
|
147
184
|
|
|
148
|
-
|
|
185
|
+
See [§How](#how) for details.
|
|
149
186
|
|
|
150
|
-
|
|
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.)
|
|
187
|
+
### 2) Enable JsonLogic Semantics (optional)
|
|
188
|
+
Enable semantics to mirror JsonLogic’s comparison/truthiness in Ruby:
|
|
216
189
|
|
|
217
|
-
|
|
190
|
+
```ruby
|
|
191
|
+
using JsonLogic::Semantics
|
|
192
|
+
```
|
|
218
193
|
|
|
219
|
-
## Extending (add your own operator)
|
|
220
194
|
|
|
221
|
-
|
|
195
|
+
See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
|
|
222
196
|
|
|
223
|
-
|
|
224
|
-
- **Value operations** inherit `JsonLogic::Operation` (engine passes values).
|
|
197
|
+
### 3) Create an Operation and provide a machine name
|
|
225
198
|
|
|
226
|
-
|
|
199
|
+
Operation methods use a consistent call shape.
|
|
200
|
+
|
|
201
|
+
- The first parameter is the **array of operator arguments**.
|
|
202
|
+
- The second is the current **data**.
|
|
227
203
|
|
|
228
|
-
|
|
204
|
+
|
|
229
205
|
|
|
230
|
-
|
|
206
|
+
Thanks to Ruby’s destructuring, you can unpack the argument array right in the method signature.
|
|
231
207
|
|
|
232
|
-
First, create the Class for you Operation based on it's type:
|
|
233
208
|
```ruby
|
|
234
209
|
class JsonLogic::Operations::StartsWith < JsonLogic::Operation
|
|
235
|
-
def self.op_name = "starts_with"
|
|
236
|
-
|
|
210
|
+
def self.op_name = "starts_with"
|
|
237
211
|
def call((str, prefix), _data)
|
|
212
|
+
# str, prefix are ALREADY evaluated to Ruby values
|
|
238
213
|
str.to_s.start_with?(prefix.to_s)
|
|
239
214
|
end
|
|
240
215
|
end
|
|
241
216
|
```
|
|
242
217
|
|
|
243
|
-
|
|
218
|
+
### 4) Register the new operation
|
|
219
|
+
|
|
244
220
|
```ruby
|
|
245
221
|
JsonLogic::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
|
|
246
222
|
```
|
|
247
223
|
|
|
248
|
-
|
|
224
|
+
After registration, use it in rules:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{ "starts_with": [ { "var": "email" }, "admin@" ] }
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Alternative — register a Proc/Lambda
|
|
231
|
+
|
|
232
|
+
The public API is class‑oriented, but **technically** you can express an Operations as a `Proc`/`Lambda` and register it through a little anonymous functions.
|
|
233
|
+
|
|
234
|
+
DSL to register callables:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
module JsonLogic
|
|
238
|
+
module DSL
|
|
239
|
+
def self.register_proc(name, lazy: false, &block)
|
|
240
|
+
base = lazy ? JsonLogic::LazyOperation : JsonLogic::Operation
|
|
241
|
+
klass = Class.new(base) do
|
|
242
|
+
define_singleton_method(:op_name) { name.to_s }
|
|
243
|
+
define_method(:call) { |args, data| block.call(args, data) }
|
|
244
|
+
end
|
|
245
|
+
JsonLogic::Engine.default.registry.register(klass)
|
|
246
|
+
klass
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
Is useful for rapid prototyping with minimal boilerplate;
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
|
|
249
258
|
```ruby
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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"
|
|
259
|
+
JsonLogic::DSL.register_proc("starts_with") do |(str, prefix), _data|
|
|
260
|
+
str.to_s.start_with?(prefix.to_s)
|
|
261
|
+
end
|
|
262
262
|
```
|
|
263
263
|
|
|
264
|
-
|
|
264
|
+
Later you can “promote” the it into a full class.
|
|
265
|
+
|
|
266
|
+
## JsonLogic Semantic
|
|
265
267
|
|
|
266
|
-
|
|
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`
|
|
268
|
+
All supported Operations follow JsonLogic semantics.
|
|
269
269
|
|
|
270
|
+
### Comparisons
|
|
271
|
+
As JsonLogic primary developed in JavaScript it inherits JavaScript's type coercion in build-in Operations. JsonLogic (JS‑style) comparisons coerce types; Ruby does not.
|
|
272
|
+
|
|
273
|
+
**JavaScript:**
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
1 >= "1.0" // true
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Ruby:**
|
|
270
280
|
|
|
271
281
|
```ruby
|
|
272
|
-
|
|
273
|
-
|
|
282
|
+
1 >= "1.0"
|
|
283
|
+
# ArgumentError: comparison of Integer with String failed
|
|
284
|
+
```
|
|
274
285
|
|
|
275
|
-
|
|
276
|
-
engine = JsonLogic::Engine.default
|
|
277
|
-
engine.evaluate(rule, data)
|
|
286
|
+
**Ruby (with JsonLogic semantics enabled):**
|
|
278
287
|
|
|
279
|
-
|
|
280
|
-
|
|
288
|
+
```ruby
|
|
289
|
+
using JsonLogic::Semantics
|
|
290
|
+
|
|
291
|
+
1 >= "1.0" # => true
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Truthiness
|
|
295
|
+
|
|
296
|
+
JsonLogic’s truthiness differs from Ruby’s (see <https://jsonlogic.com/truthy.html>).
|
|
297
|
+
In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are also falsey.
|
|
298
|
+
|
|
299
|
+
**In Ruby:**
|
|
300
|
+
```ruby
|
|
301
|
+
!![]
|
|
302
|
+
# => true
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
While JsonLogic as was mentioned before has it's own truthiness:
|
|
306
|
+
|
|
307
|
+
**In Ruby (with JsonLogic Semantic):**
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
include JsonLogic::Semantics
|
|
311
|
+
|
|
312
|
+
truthy?([])
|
|
313
|
+
# => false
|
|
281
314
|
```
|
|
282
315
|
|
|
283
|
-
---
|
|
284
316
|
|
|
285
317
|
## Compliance and tests
|
|
318
|
+
|
|
286
319
|
Optional: quick self-test
|
|
287
320
|
|
|
321
|
+
|
|
322
|
+
|
|
288
323
|
```bash
|
|
289
324
|
ruby test/selftest.rb
|
|
290
325
|
```
|
|
291
326
|
|
|
327
|
+
|
|
292
328
|
Official test suite
|
|
329
|
+
|
|
293
330
|
1. Fetch the official suite
|
|
294
331
|
|
|
332
|
+
|
|
333
|
+
|
|
295
334
|
```bash
|
|
296
335
|
mkdir -p spec/tmp
|
|
297
336
|
curl -fsSL https://jsonlogic.com/tests.json -o spec/tmp/tests.json
|
|
298
337
|
```
|
|
338
|
+
|
|
299
339
|
2. Run it
|
|
340
|
+
|
|
300
341
|
```bash
|
|
301
342
|
ruby script/compliance.rb spec/tmp/tests.json
|
|
302
343
|
```
|
|
303
344
|
|
|
304
|
-
Expected output
|
|
345
|
+
Expected output
|
|
346
|
+
|
|
305
347
|
```bash
|
|
306
348
|
# => Compliance: X/X passed
|
|
307
349
|
```
|
|
308
350
|
|
|
309
|
-
---
|
|
310
|
-
|
|
311
351
|
## Security
|
|
312
352
|
|
|
313
353
|
- Rules are **data**, not code; no Ruby eval.
|
|
314
|
-
-
|
|
315
|
-
-
|
|
354
|
+
- Operations are **pure** (no IO, no network, no shell).
|
|
355
|
+
- Rules have **no write** access to anything.
|
|
356
|
+
|
|
357
|
+
## License
|
|
316
358
|
|
|
359
|
+
MIT — see [LICENSE](LICENSE).
|
|
317
360
|
|
|
318
361
|
## Authors
|
|
319
362
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::All < JsonLogic::EnumerableOperation
|
|
4
6
|
def self.op_name = "all"
|
|
5
7
|
|
|
@@ -8,7 +10,7 @@ class JsonLogic::Operations::All < JsonLogic::EnumerableOperation
|
|
|
8
10
|
return false if items.empty?
|
|
9
11
|
|
|
10
12
|
items.all? do |item|
|
|
11
|
-
|
|
13
|
+
!!JsonLogic.apply(rule_applied_to_each_item, item)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::And < JsonLogic::LazyOperation
|
|
4
6
|
def self.op_name = "and"
|
|
5
7
|
|
|
@@ -7,7 +9,7 @@ class JsonLogic::Operations::And < JsonLogic::LazyOperation
|
|
|
7
9
|
last = nil
|
|
8
10
|
args.each do |a|
|
|
9
11
|
last = JsonLogic.apply(a, data)
|
|
10
|
-
return last unless
|
|
12
|
+
return last unless !!last
|
|
11
13
|
end
|
|
12
14
|
last
|
|
13
15
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::Filter < JsonLogic::EnumerableOperation
|
|
4
6
|
def self.op_name = "filter"
|
|
5
7
|
|
|
@@ -7,7 +9,7 @@ class JsonLogic::Operations::Filter < JsonLogic::EnumerableOperation
|
|
|
7
9
|
items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
|
|
8
10
|
|
|
9
11
|
items.filter do |item|
|
|
10
|
-
|
|
12
|
+
!!JsonLogic.apply(rule_applied_to_each_item, item)
|
|
11
13
|
end
|
|
12
14
|
end
|
|
13
15
|
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::If < JsonLogic::LazyOperation
|
|
4
6
|
def self.op_name = "if"
|
|
5
7
|
|
|
6
8
|
def call(args, data)
|
|
7
9
|
i = 0
|
|
8
10
|
while i < args.size - 1
|
|
9
|
-
return JsonLogic.apply(args[i + 1], data) if
|
|
11
|
+
return JsonLogic.apply(args[i + 1], data) if !!(JsonLogic.apply(args[i], data))
|
|
10
12
|
i += 2
|
|
11
13
|
end
|
|
12
14
|
return JsonLogic.apply(args[-1], data) if args.size.odd?
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::None < JsonLogic::EnumerableOperation
|
|
4
6
|
def self.op_name = "none"
|
|
5
7
|
|
|
6
8
|
def call(args, data)
|
|
7
9
|
items, rule_applied_to_each_item = resolve_items_and_per_item_rule(args, data)
|
|
8
10
|
items.none? do |item|
|
|
9
|
-
|
|
11
|
+
!!(JsonLogic.apply(rule_applied_to_each_item, item))
|
|
10
12
|
end
|
|
11
13
|
end
|
|
12
14
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
using JsonLogic::Semantics
|
|
2
4
|
class JsonLogic::Operations::Or < JsonLogic::LazyOperation
|
|
3
5
|
def self.op_name = "or"
|
|
4
6
|
|
|
5
7
|
def call(args, data)
|
|
6
8
|
args.each do |a|
|
|
7
9
|
v = JsonLogic.apply(a, data)
|
|
8
|
-
return v if
|
|
10
|
+
return v if !!v
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
args.empty? ? nil : JsonLogic.apply(args.last, data)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: tru
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::Some < JsonLogic::EnumerableOperation
|
|
4
6
|
def self.op_name = "some"
|
|
5
7
|
|
|
@@ -8,7 +10,7 @@ class JsonLogic::Operations::Some < JsonLogic::EnumerableOperation
|
|
|
8
10
|
return false if items.empty?
|
|
9
11
|
|
|
10
12
|
items.any? do |item|
|
|
11
|
-
|
|
13
|
+
!!JsonLogic.apply(rule_applied_to_each_item, item)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::Ternary < JsonLogic::LazyOperation
|
|
4
6
|
def self.op_name = "?:"
|
|
5
7
|
|
|
6
8
|
def call((cond_rule, then_rule, else_rule), data)
|
|
7
|
-
if
|
|
9
|
+
if !!JsonLogic.apply(cond_rule, data)
|
|
8
10
|
JsonLogic.apply(then_rule, data)
|
|
9
11
|
else
|
|
10
12
|
JsonLogic.apply(else_rule, data)
|
data/lib/json_logic/semantics.rb
CHANGED
|
@@ -6,11 +6,18 @@ module JsonLogic
|
|
|
6
6
|
|
|
7
7
|
def truthy?(v)
|
|
8
8
|
case v
|
|
9
|
-
when nil
|
|
10
|
-
|
|
11
|
-
when
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
when nil
|
|
10
|
+
false
|
|
11
|
+
when TrueClass, FalseClass
|
|
12
|
+
v
|
|
13
|
+
when Numeric
|
|
14
|
+
v.zero? ? false : true
|
|
15
|
+
when String
|
|
16
|
+
v.empty? ? false : true
|
|
17
|
+
when Array
|
|
18
|
+
v.empty? ? false : true
|
|
19
|
+
else
|
|
20
|
+
true
|
|
14
21
|
end
|
|
15
22
|
end
|
|
16
23
|
|
|
@@ -24,7 +31,11 @@ module JsonLogic
|
|
|
24
31
|
when String
|
|
25
32
|
s = v.strip
|
|
26
33
|
return 0.0 if s.empty?
|
|
27
|
-
|
|
34
|
+
begin
|
|
35
|
+
Float(s)
|
|
36
|
+
rescue ArgumentError
|
|
37
|
+
Float::NAN
|
|
38
|
+
end
|
|
28
39
|
else
|
|
29
40
|
Float::NAN
|
|
30
41
|
end
|
|
@@ -32,11 +43,15 @@ module JsonLogic
|
|
|
32
43
|
|
|
33
44
|
def eq(a, b)
|
|
34
45
|
if a.class == b.class
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
if a.is_a?(Numeric)
|
|
47
|
+
return false if a.to_f.nan? || b.to_f.nan?
|
|
48
|
+
return a.to_f == b.to_f
|
|
49
|
+
else
|
|
50
|
+
return a.eql?(b)
|
|
51
|
+
end
|
|
37
52
|
end
|
|
38
53
|
|
|
39
|
-
if a
|
|
54
|
+
if a.is_a?(TrueClass) || a.is_a?(FalseClass) || b.is_a?(TrueClass) || b.is_a?(FalseClass)
|
|
40
55
|
na = num(a); nb = num(b)
|
|
41
56
|
return false if na.nan? || nb.nan?
|
|
42
57
|
return na == nb
|
|
@@ -61,29 +76,29 @@ module JsonLogic
|
|
|
61
76
|
end
|
|
62
77
|
end
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def >(other)
|
|
69
|
-
c = JsonLogic::Semantics.cmp(self, other)
|
|
70
|
-
c && c == 1
|
|
71
|
-
end
|
|
79
|
+
refine Object do
|
|
80
|
+
def !@
|
|
81
|
+
JsonLogic::Semantics.truthy?(self) ? false : true
|
|
82
|
+
end
|
|
72
83
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
84
|
+
def to_bool
|
|
85
|
+
JsonLogic::Semantics.truthy?(self)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
[String, Integer, Float].each do |klass|
|
|
90
|
+
refine klass do
|
|
91
|
+
def ==(other) = JsonLogic::Semantics.eq(self, other)
|
|
92
|
+
def >(other) = (c = JsonLogic::Semantics.cmp(self, other)) && c == 1
|
|
93
|
+
def >=(other) = (c = JsonLogic::Semantics.cmp(self, other)) && (c == 1 || c == 0)
|
|
94
|
+
def <(other) = (c = JsonLogic::Semantics.cmp(self, other)) && c == -1
|
|
95
|
+
def <=(other) = (c = JsonLogic::Semantics.cmp(self, other)) && (c == -1 || c == 0)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
82
98
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
end
|
|
99
|
+
[Array, TrueClass, FalseClass, NilClass].each do |klass|
|
|
100
|
+
refine klass do
|
|
101
|
+
def ==(other) = JsonLogic::Semantics.eq(self, other)
|
|
87
102
|
end
|
|
88
103
|
end
|
|
89
104
|
end
|
data/lib/json_logic/version.rb
CHANGED