json-logic-rb 0.1.5 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +197 -197
- data/lib/json_logic/engine.rb +22 -21
- data/lib/json_logic/enumerable_operation.rb +27 -5
- data/lib/json_logic/errors/error.rb +29 -0
- data/lib/json_logic/errors/invalid_arguments_error.rb +7 -0
- data/lib/json_logic/errors/logic_error.rb +7 -0
- data/lib/json_logic/errors/nan_error.rb +7 -0
- data/lib/json_logic/ext/array.rb +5 -0
- data/lib/json_logic/operations/add.rb +3 -3
- data/lib/json_logic/operations/all.rb +3 -6
- data/lib/json_logic/operations/and.rb +6 -5
- data/lib/json_logic/operations/bool_cast.rb +2 -3
- data/lib/json_logic/operations/cat.rb +3 -1
- data/lib/json_logic/operations/coalesce.rb +9 -0
- data/lib/json_logic/operations/div.rb +7 -1
- data/lib/json_logic/operations/equal.rb +12 -3
- data/lib/json_logic/operations/exists.rb +14 -0
- data/lib/json_logic/operations/filter.rb +11 -3
- data/lib/json_logic/operations/gt.rb +12 -4
- data/lib/json_logic/operations/gte.rb +12 -4
- data/lib/json_logic/operations/if.rb +2 -0
- data/lib/json_logic/operations/in.rb +2 -0
- data/lib/json_logic/operations/lt.rb +10 -4
- data/lib/json_logic/operations/lte.rb +12 -4
- data/lib/json_logic/operations/map.rb +14 -2
- data/lib/json_logic/operations/max.rb +3 -1
- data/lib/json_logic/operations/merge.rb +4 -3
- data/lib/json_logic/operations/min.rb +3 -1
- data/lib/json_logic/operations/missing.rb +4 -26
- data/lib/json_logic/operations/missing_some.rb +6 -20
- data/lib/json_logic/operations/mod.rb +9 -1
- data/lib/json_logic/operations/mul.rb +4 -1
- data/lib/json_logic/operations/none.rb +4 -5
- data/lib/json_logic/operations/not_equal.rb +12 -3
- data/lib/json_logic/operations/or.rb +5 -1
- data/lib/json_logic/operations/preserve.rb +9 -0
- data/lib/json_logic/operations/reduce.rb +21 -5
- data/lib/json_logic/operations/some.rb +4 -7
- data/lib/json_logic/operations/strict_equal.rb +26 -3
- data/lib/json_logic/operations/strict_not_equal.rb +24 -3
- data/lib/json_logic/operations/sub.rb +8 -1
- data/lib/json_logic/operations/substr.rb +12 -20
- data/lib/json_logic/operations/ternary.rb +1 -7
- data/lib/json_logic/operations/throw.rb +12 -0
- data/lib/json_logic/operations/try.rb +35 -0
- data/lib/json_logic/operations/val.rb +79 -0
- data/lib/json_logic/operations/var.rb +22 -20
- data/lib/json_logic/scope.rb +67 -0
- data/lib/json_logic/semantics.rb +107 -38
- data/lib/json_logic/tree.rb +97 -0
- data/lib/json_logic/version.rb +1 -1
- data/lib/json_logic.rb +12 -0
- data/script/build_tests_json.rb +26 -0
- data/script/compliance.rb +160 -37
- data/spec/tmp/v2/tests.json +16981 -0
- metadata +24 -13
- /data/spec/tmp/{tests.json → v1/tests.json} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf7e4291a1fe0a56b1387ca35f03d239679f4d0c76333a3d0d0f3933efeead0b
|
|
4
|
+
data.tar.gz: 134bb254ff81c389daa1d9527b9496d17d0417316143586ea8ade75c739f2657
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a98a01a255cdce55c19940171b00fcf652e9735369e6316d747cbfc70108ed42c207d8beca244bf042b47f0a7aa978b962e523fed9662bc2d35970f5476d6b4
|
|
7
|
+
data.tar.gz: f2a6bedd19099407dbd4398647553cbe9a341926b71b0cdd6c50f99fbe856015ff20a8408087e295c4a459dd13d2c2cdf360810b9af4ceff07e98e102e8012da
|
data/README.md
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
+
[![jsonlogic core][src-core]](https://jsonlogic.com/tests.json) [![jsonlogic community][src-community]](https://github.com/json-logic/compat-tables/tree/main/suites) [](https://github.com/tavrelkate/json-logic-rb/actions/workflows/compliance.yml?query=branch%3Amain) <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>
|
|
1
2
|
|
|
2
3
|
# json-logic-rb
|
|
3
4
|
|
|
4
|
-
Ruby implementation of [JsonLogic](https://jsonlogic.com/)
|
|
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>
|
|
5
|
+
Ruby implementation of [JsonLogic](https://jsonlogic.com/). Pure and extensible. Full compliance with both core and community-extended specifications.
|
|
7
6
|
|
|
8
7
|
## Table of Contents
|
|
9
8
|
- [What](#what)
|
|
10
9
|
- [Install](#install)
|
|
11
10
|
- [Quick start](#quick-start)
|
|
12
|
-
- [
|
|
11
|
+
- [Complience](#complience)
|
|
12
|
+
- [Supported Operations (Built-in)](#supported-operations-built-in)
|
|
13
|
+
- [Adding Operations](#adding-operations)
|
|
14
|
+
- [Enable JsonLogic Semantics (optional)](#enable-jsonlogic-semantics-optional)
|
|
15
|
+
- [Parameters](#parameters)
|
|
16
|
+
- [Proc / Lambda](#proc--lambda)
|
|
17
|
+
- [Class](#class)
|
|
18
|
+
- [Laziness](#laziness)
|
|
13
19
|
- [1. Default Operations](#1-default-operations)
|
|
14
20
|
- [2. Lazy Operations](#2-lazy-operations)
|
|
15
|
-
- [
|
|
16
|
-
- [Adding Operations](#adding-operations)
|
|
21
|
+
- [Why laziness matters?](#why-laziness-matters)
|
|
17
22
|
- [JsonLogic Semantic](#jsonlogic-semantic)
|
|
18
|
-
- [
|
|
23
|
+
- [Comparisons](#comparisons)
|
|
24
|
+
- [Truthiness](#truthiness)
|
|
19
25
|
- [Security](#security)
|
|
20
26
|
- [License](#license)
|
|
21
27
|
- [Authors](#authors)
|
|
@@ -27,9 +33,20 @@ JsonLogic rules are JSON trees. The engine walks that tree and returns a Ruby va
|
|
|
27
33
|
|
|
28
34
|
## Install
|
|
29
35
|
|
|
36
|
+
Download the gem locally
|
|
30
37
|
```bash
|
|
31
38
|
gem install json-logic-rb
|
|
32
39
|
```
|
|
40
|
+
If needed – add to your Gemfile
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
gem "json-logic-rb"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then install
|
|
47
|
+
```shell
|
|
48
|
+
bundle install
|
|
49
|
+
```
|
|
33
50
|
|
|
34
51
|
## Quick start
|
|
35
52
|
|
|
@@ -45,214 +62,230 @@ JsonLogic.apply(rule)
|
|
|
45
62
|
With data:
|
|
46
63
|
|
|
47
64
|
```ruby
|
|
48
|
-
|
|
65
|
+
require 'json_logic'
|
|
66
|
+
|
|
67
|
+
rule = { "var" => "user.age" }
|
|
68
|
+
data = { "user" => { "age" => 42 } }
|
|
69
|
+
|
|
70
|
+
JsonLogic.apply(rule, data)
|
|
49
71
|
# => 42
|
|
50
72
|
```
|
|
51
73
|
|
|
52
|
-
## How
|
|
53
74
|
|
|
54
|
-
There are **two types of operations** in this implementation: Default Operations and Lazy Operations.
|
|
55
75
|
|
|
56
|
-
### 1. Default Operations
|
|
57
76
|
|
|
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.
|
|
60
77
|
|
|
61
|
-
|
|
78
|
+
## Complience
|
|
79
|
+
|
|
80
|
+
The JsonLogic specification provides test suites — concrete inputs with expected outputs that validate the implementation of operations. The specification comes in two variants:
|
|
81
|
+
- [![jsonlogic core][src-core]](https://jsonlogic.com/tests.json) – [original JsonLogic website](https://jsonlogic.com/tests.json)
|
|
82
|
+
- [![jsonlogic community][src-community]](https://github.com/json-logic/compat-tables/tree/main/suites) – [extensions built on top of the core](https://github.com/json-logic/compat-tables/tree/main/suites)
|
|
83
|
+
|
|
84
|
+
The "extra" exists because "core" hasn't changed in years — and that’s fine, "core" is a solid, finished foundation. Think of it as v1, while "extra" is the v2+ evolution as there are no visible plans to change the original.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Supported Operations (Built-in)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
| Operator | Supported | Source |
|
|
93
|
+
|---|---:|---|
|
|
94
|
+
| [Data / Presence](https://jsonlogic.com/operations.html#accessing-data) | | |
|
|
95
|
+
| `var` | ✅ |  |
|
|
96
|
+
| `val` | ✅ |  |
|
|
97
|
+
| `missing` | ✅ |  |
|
|
98
|
+
| `missing_some` | ✅ |  |
|
|
99
|
+
| `exists` | ✅ |  |
|
|
100
|
+
| [Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) | | |
|
|
101
|
+
| `if` | ✅ |  |
|
|
102
|
+
| `?:` | ✅ |  |
|
|
103
|
+
| `and` | ✅ |  |
|
|
104
|
+
| `or` | ✅ |  |
|
|
105
|
+
| `!` | ✅ |  |
|
|
106
|
+
| `!!` | ✅ |  |
|
|
107
|
+
| [Comparison Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) | | |
|
|
108
|
+
| `==` | ✅ |  |
|
|
109
|
+
| `===` | ✅ |  |
|
|
110
|
+
| `!=` | ✅ |  |
|
|
111
|
+
| `!==` | ✅ |  |
|
|
112
|
+
| `>` | ✅ |  |
|
|
113
|
+
| `>=` | ✅ |  |
|
|
114
|
+
| `<` | ✅ |  |
|
|
115
|
+
| `<=` | ✅ |  |
|
|
116
|
+
| [Numeric Operations](https://jsonlogic.com/operations.html#numeric-operations) | | |
|
|
117
|
+
| `+` | ✅ |  |
|
|
118
|
+
| `-` | ✅ |  |
|
|
119
|
+
| `*` | ✅ |  |
|
|
120
|
+
| `/` | ✅ |  |
|
|
121
|
+
| `%` | ✅ |  |
|
|
122
|
+
| `min` | ✅ |  |
|
|
123
|
+
| `max` | ✅ |  |
|
|
124
|
+
| [Array Operations](https://jsonlogic.com/operations.html#array-operations) | | |
|
|
125
|
+
| `map` | ✅ |  |
|
|
126
|
+
| `reduce` | ✅ |  |
|
|
127
|
+
| `filter` | ✅ |  |
|
|
128
|
+
| `all` | ✅ |  |
|
|
129
|
+
| `none` | ✅ |  |
|
|
130
|
+
| `some` | ✅ |  |
|
|
131
|
+
| `merge` | ✅ |  |
|
|
132
|
+
| [String Operations](https://jsonlogic.com/operations.html#string-operations) | | |
|
|
133
|
+
| `in` | ✅ |  |
|
|
134
|
+
| `cat` | ✅ |  |
|
|
135
|
+
| `substr` | ✅ |  |
|
|
136
|
+
| [Community Extensions](https://github.com/json-logic/compat-tables/tree/main/suites) | | |
|
|
137
|
+
| `??` | ✅ |  |
|
|
138
|
+
| `try` | ✅ |  |
|
|
139
|
+
| `throw` | ✅ |  |
|
|
140
|
+
| `preserve` | ✅ |  |
|
|
141
|
+
| Docs-only / Not implemented | | |
|
|
142
|
+
| `log` | 🚫 |  |
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
[src-core]: https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square
|
|
146
|
+
[src-community]: https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square
|
|
62
147
|
|
|
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`.
|
|
66
148
|
|
|
67
|
-
|
|
149
|
+
## Adding Operations
|
|
68
150
|
|
|
69
|
-
|
|
151
|
+
Don’t expect JsonLogic to include every specialized operation. It’s intentionally small and not a programming language. It will never do everything.
|
|
70
152
|
|
|
71
|
-
|
|
153
|
+
Before you reach for a custom solution, see if you can express your logic using the [§Supported Operations (Built‑in)](https://www.google.com/search?q=%23supported-operations-built-in). Often, a simple change in perspective is all you need to get the job done with what's already there.
|
|
72
154
|
|
|
73
|
-
|
|
74
|
-
[Logic & boolean operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) • [Truthiness](https://jsonlogic.com/truthy.html)
|
|
155
|
+
If that doesn't cut it, adding a custom operation is straightforward. Keep it simple: start with a Proc or a Lambda. If needed – promote it to a Class.
|
|
75
156
|
|
|
76
|
-
- **Enumerable operators** — `map`, `filter`, `reduce`, `all`, `none`, `some`
|
|
77
|
-
[Array operations](https://jsonlogic.com/operations.html#array-operations)
|
|
78
157
|
|
|
79
|
-
|
|
158
|
+
### Enable JsonLogic Semantics (optional)
|
|
159
|
+
Enable semantics to mirror JsonLogic’s comparison and truthiness in Ruby.
|
|
80
160
|
|
|
81
|
-
|
|
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"`.
|
|
161
|
+
See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
|
|
84
162
|
|
|
85
163
|
|
|
86
|
-
|
|
164
|
+
### Parameters
|
|
87
165
|
|
|
88
|
-
|
|
89
|
-
# filter: keep numbers >= 2
|
|
90
|
-
JsonLogic.apply(
|
|
91
|
-
{ "filter" => [ { "var" => "ints" }, { ">=" => [ { "var" => "" }, 2 ] } ] },
|
|
92
|
-
{ "ints" => [1,2,3] }
|
|
93
|
-
)
|
|
94
|
-
# => [2, 3]
|
|
95
|
-
```
|
|
166
|
+
Operator function use a consistent call shape:
|
|
96
167
|
|
|
97
|
-
**
|
|
168
|
+
- First parameter: **array of operator arguments** (you can destructure it).
|
|
98
169
|
|
|
170
|
+
- Second parameter: current **data**.
|
|
99
171
|
```ruby
|
|
100
|
-
|
|
101
|
-
JsonLogic.apply(
|
|
102
|
-
{ "reduce" => [
|
|
103
|
-
{ "var" => "ints" },
|
|
104
|
-
{ "+" => [ { "var" => "accumulator" }, { "var" => "current" } ] }, 0 ]
|
|
105
|
-
},
|
|
106
|
-
{ "ints" => [1,2,3,4] }
|
|
107
|
-
)
|
|
108
|
-
# => 10.0
|
|
172
|
+
->((string, prefix), data) { string.to_s.start_with?(prefix.to_s) }
|
|
109
173
|
```
|
|
110
174
|
|
|
111
|
-
###
|
|
175
|
+
### Proc / Lambda
|
|
176
|
+
|
|
177
|
+
Pick the Operation type.
|
|
112
178
|
|
|
113
|
-
|
|
179
|
+
[Default Operation](#1-default-operations) mode passes values.
|
|
114
180
|
|
|
115
181
|
```ruby
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# => 1
|
|
182
|
+
JsonLogic.add_operation("starts_with") do |(string_value, prefix_value), _data|
|
|
183
|
+
string_value.to_s.start_with?(prefix_value.to_s)
|
|
184
|
+
end
|
|
120
185
|
```
|
|
186
|
+
[Lazy Operation](#2-lazy-operations) mode passes raw rules (you evaluate them):
|
|
121
187
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
188
|
+
```ruby
|
|
189
|
+
JsonLogic.add_operation("starts_with", lazy: true) do |(string_rule, prefix_rule), data|
|
|
190
|
+
string_value = JsonLogic.apply(string_rule, data)
|
|
191
|
+
prefix_value = JsonLogic.apply(prefix_rule, data)
|
|
192
|
+
string_value.to_s.start_with?(prefix_value.to_s)
|
|
193
|
+
end
|
|
194
|
+
```
|
|
173
195
|
|
|
174
|
-
|
|
196
|
+
See [§Laziness](#laziness) for details.
|
|
175
197
|
|
|
176
|
-
|
|
198
|
+
Use immediately:
|
|
177
199
|
|
|
178
200
|
```ruby
|
|
179
|
-
JsonLogic.
|
|
201
|
+
JsonLogic.apply({ "starts_with" => [ { "var" => "email" }, "admin@" ] })
|
|
180
202
|
```
|
|
181
203
|
|
|
182
|
-
Once the function added, you can use it in your logic.
|
|
183
204
|
|
|
184
|
-
|
|
185
|
-
JsonLogic.apply({ "times2" => [21] })
|
|
186
|
-
# => 42
|
|
187
|
-
```
|
|
205
|
+
### Class
|
|
188
206
|
|
|
189
|
-
|
|
190
|
-
Later you can “promote” it into a full class or use additional features.
|
|
207
|
+
Pick the Operation type. It has the same call shape.
|
|
191
208
|
|
|
209
|
+
[Default Operation](#1-default-operations) – Inherit `JsonLogic::Operation`.
|
|
192
210
|
|
|
193
|
-
### 1) Pick the Operation type
|
|
194
|
-
Choose one of:
|
|
195
|
-
- **Default**
|
|
196
|
-
```ruby
|
|
197
|
-
class JsonLogic::Operations::StartsWith < JsonLogic::Operation; end
|
|
198
|
-
```
|
|
199
|
-
For anonymous functions:
|
|
200
211
|
```ruby
|
|
201
|
-
JsonLogic
|
|
212
|
+
class JsonLogic::Operations::StartsWith < JsonLogic::Operation
|
|
213
|
+
def self.name = "starts_with"
|
|
214
|
+
def call(string_value, prefix_value), _data) = string_value.to_s.start_with?(prefix_value.to_s)
|
|
215
|
+
end
|
|
202
216
|
```
|
|
203
|
-
|
|
217
|
+
|
|
218
|
+
[Lazy Operation](#2-lazy-operations) – Inherit `JsonLogic::LazyOperation`.
|
|
219
|
+
|
|
220
|
+
Register explicitly:
|
|
221
|
+
|
|
204
222
|
```ruby
|
|
205
|
-
|
|
223
|
+
JsonLogic::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
|
|
206
224
|
```
|
|
207
|
-
|
|
225
|
+
|
|
226
|
+
Now, Class is ready to use.
|
|
227
|
+
|
|
208
228
|
```ruby
|
|
209
|
-
JsonLogic.
|
|
229
|
+
JsonLogic.apply({ "starts_with" => [ { "var" => "email" }, "admin@" ] })
|
|
210
230
|
```
|
|
211
231
|
|
|
212
|
-
See [§How](#how) for details.
|
|
213
232
|
|
|
214
|
-
### 2) Enable JsonLogic Semantics (optional)
|
|
215
|
-
Enable semantics to mirror JsonLogic’s comparison/truthiness in Ruby:
|
|
216
233
|
|
|
217
|
-
```ruby
|
|
218
|
-
using JsonLogic::Semantics
|
|
219
|
-
```
|
|
220
234
|
|
|
221
235
|
|
|
222
|
-
See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
|
|
223
236
|
|
|
224
|
-
### 3) Create an Operation and provide a machine name
|
|
225
237
|
|
|
226
|
-
Operation methods use a consistent call shape.
|
|
227
238
|
|
|
228
|
-
- The first parameter is the **array of operator arguments**.
|
|
229
|
-
- The second is the current **data**.
|
|
230
239
|
|
|
240
|
+
## Laziness
|
|
231
241
|
|
|
242
|
+
There are two types of operations: [Default Operations](#1-default-operations) and [Lazy Operations](#2-lazy-operations).
|
|
232
243
|
|
|
233
|
-
|
|
244
|
+
### 1. Default Operations
|
|
245
|
+
|
|
246
|
+
For **Default Operations**, the it evaluates all arguments first and then calls the operator with the resulting Ruby values.
|
|
247
|
+
This matches the reference behavior for arithmetic, comparisons, string operations, and other pure operations that do not control evaluation order.
|
|
248
|
+
|
|
249
|
+
**Groups and references:**
|
|
250
|
+
|
|
251
|
+
- [Numeric operations](https://jsonlogic.com/operations.html#numeric-operations)
|
|
252
|
+
- [String operations](https://jsonlogic.com/operations.html#string-operations)
|
|
253
|
+
- [Array operations](https://jsonlogic.com/operations.html#array-operations) — simple transform like `merge`.
|
|
254
|
+
|
|
255
|
+
### 2. Lazy Operations
|
|
256
|
+
|
|
257
|
+
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 data. The operator then evaluates only the sub-rules it actually needs.
|
|
258
|
+
|
|
259
|
+
**Groups and references:**
|
|
260
|
+
|
|
261
|
+
- [Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) — short-circuit/branching like `or`.
|
|
262
|
+
- [Comparison operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) — equality/ordering like `==`.
|
|
263
|
+
- [Array operations](https://jsonlogic.com/operations.html#array-operations) — enumerable evaluation like `map`.
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
**Example #1**
|
|
234
267
|
|
|
235
268
|
```ruby
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
end
|
|
269
|
+
# filter: keep numbers >= 2
|
|
270
|
+
JsonLogic.apply(
|
|
271
|
+
{ "filter" => [ { "var" => "ints" }, { ">=" => [ { "var" => "" }, 2 ] } ] },
|
|
272
|
+
{ "ints" => [1,2,3] }
|
|
273
|
+
)
|
|
274
|
+
# => [2, 3]
|
|
243
275
|
```
|
|
244
276
|
|
|
245
|
-
###
|
|
277
|
+
### Why laziness matters?
|
|
278
|
+
|
|
279
|
+
Lazy operations prevent evaluation of branches you do not need.
|
|
246
280
|
|
|
281
|
+
If hypothetically division by zero raises an error, lazy control would avoid it.
|
|
247
282
|
```ruby
|
|
248
|
-
JsonLogic
|
|
283
|
+
JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
|
|
284
|
+
# => 1
|
|
249
285
|
```
|
|
250
286
|
|
|
251
|
-
|
|
287
|
+
> In this gem division returns nil on divide‑by‑zero, but this example show why lazy evaluation is required by the spec: branching and boolean operators must not evaluate unused branches.
|
|
252
288
|
|
|
253
|
-
```json
|
|
254
|
-
{ "starts_with": [ { "var": "email" }, "admin@" ] }
|
|
255
|
-
```
|
|
256
289
|
|
|
257
290
|
|
|
258
291
|
|
|
@@ -281,13 +314,14 @@ As JsonLogic primary developed in JavaScript it inherits JavaScript's type coerc
|
|
|
281
314
|
```ruby
|
|
282
315
|
using JsonLogic::Semantics
|
|
283
316
|
|
|
284
|
-
1 >= "1.0"
|
|
317
|
+
1 >= "1.0"
|
|
318
|
+
# => true
|
|
285
319
|
```
|
|
286
320
|
|
|
287
321
|
### Truthiness
|
|
288
322
|
|
|
289
|
-
JsonLogic’s truthiness differs from Ruby’s (see
|
|
290
|
-
In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are
|
|
323
|
+
JsonLogic’s truthiness differs from Ruby’s (see [Json Logic Website Truthy and Falsy](https://jsonlogic.com/truthy.html)).
|
|
324
|
+
In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are falsey too.
|
|
291
325
|
|
|
292
326
|
**In Ruby:**
|
|
293
327
|
```ruby
|
|
@@ -295,63 +329,29 @@ In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty
|
|
|
295
329
|
# => true
|
|
296
330
|
```
|
|
297
331
|
|
|
298
|
-
While JsonLogic as was mentioned before has it's own truthiness
|
|
332
|
+
While JsonLogic as was mentioned before has it's own truthiness.
|
|
299
333
|
|
|
300
334
|
**In Ruby (with JsonLogic Semantic):**
|
|
301
335
|
|
|
302
336
|
```ruby
|
|
303
|
-
|
|
337
|
+
using JsonLogic::Semantics
|
|
304
338
|
|
|
305
|
-
|
|
339
|
+
!![]
|
|
306
340
|
# => false
|
|
307
341
|
```
|
|
308
342
|
|
|
309
|
-
|
|
310
|
-
## Compliance and tests
|
|
311
|
-
|
|
312
|
-
Optional: quick self-test
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
ruby test/selftest.rb
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
Official test suite
|
|
322
|
-
|
|
323
|
-
1. Fetch the official suite
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
```bash
|
|
328
|
-
mkdir -p spec/tmp
|
|
329
|
-
curl -fsSL https://jsonlogic.com/tests.json -o spec/tmp/tests.json
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
2. Run it
|
|
333
|
-
|
|
334
|
-
```bash
|
|
335
|
-
ruby script/compliance.rb spec/tmp/tests.json
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
Expected output
|
|
339
|
-
|
|
340
|
-
```bash
|
|
341
|
-
# => Compliance: X/X passed
|
|
342
|
-
```
|
|
343
|
-
|
|
344
343
|
## Security
|
|
345
344
|
|
|
346
|
-
-
|
|
347
|
-
-
|
|
348
|
-
-
|
|
345
|
+
- RULES ARE DATA; NO RUBY EVAL;
|
|
346
|
+
- OPERATIONS ARE PURE; NO IO, NO NETWORK; NO SHELL;
|
|
347
|
+
- RULES HAVE NO WRITE ACCESS TO ANYTHING;
|
|
348
|
+
|
|
349
349
|
|
|
350
350
|
## License
|
|
351
351
|
|
|
352
352
|
MIT — see [LICENSE](LICENSE).
|
|
353
353
|
|
|
354
|
-
##
|
|
354
|
+
## Maintainers
|
|
355
355
|
|
|
356
|
-
- [Valeriya Petrova](https://github.com/piatrova
|
|
356
|
+
- [Valeriya Petrova](https://github.com/valeryia-piatrova)
|
|
357
357
|
- [Tavrel Kate](https://github.com/tavrelkate)
|
data/lib/json_logic/engine.rb
CHANGED
|
@@ -17,34 +17,35 @@ module JsonLogic
|
|
|
17
17
|
attr_reader :registry
|
|
18
18
|
|
|
19
19
|
def evaluate(rule, data = nil)
|
|
20
|
-
data ||= {}
|
|
21
|
-
|
|
22
20
|
case rule
|
|
23
|
-
when Numeric,
|
|
24
|
-
String,
|
|
25
|
-
TrueClass,
|
|
26
|
-
FalseClass,
|
|
27
|
-
NilClass
|
|
21
|
+
when Numeric, String, TrueClass, FalseClass, NilClass
|
|
28
22
|
rule
|
|
29
23
|
when Array
|
|
30
24
|
rule.map { |r| evaluate(r, data) }
|
|
31
25
|
when Hash
|
|
26
|
+
unless rule.one?
|
|
27
|
+
return rule.transform_values { |value| evaluate(value, data) }
|
|
28
|
+
end
|
|
29
|
+
|
|
32
30
|
name, raw_args = rule.first
|
|
33
31
|
op_class = @registry.fetch(name)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
32
|
+
unless op_class
|
|
33
|
+
return rule.transform_values { |value| evaluate(value, data) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
args = op_class.values_only? ? Array.wrap(evaluate(raw_args, data)) : raw_args
|
|
37
|
+
begin
|
|
38
|
+
result = op_class.new.call(args, data)
|
|
39
|
+
raise JsonLogic::NaNError.new if result.is_a?(Float) && (result.nan? || result.infinite?)
|
|
40
|
+
result
|
|
41
|
+
rescue JsonLogic::Error
|
|
42
|
+
raise
|
|
43
|
+
rescue ArgumentError, IndexError, TypeError, NoMethodError
|
|
44
|
+
raise JsonLogic::InvalidArgumentsError.new
|
|
45
|
+
rescue ZeroDivisionError, FloatDomainError
|
|
46
|
+
raise JsonLogic::NaNError.new
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
raise JsonLogic::Error.new(e.message.to_s)
|
|
48
49
|
end
|
|
49
50
|
else
|
|
50
51
|
rule
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
class JsonLogic::EnumerableOperation < JsonLogic::LazyOperation
|
|
2
|
-
|
|
2
|
+
def call(args, data)
|
|
3
|
+
raise JsonLogic::InvalidArgumentsError.new unless args.is_a?(Array) && args.size >= 2
|
|
4
|
+
raise JsonLogic::InvalidArgumentsError.new if args[0].nil?
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
items, rule = extract_items_and_rule(args, data)
|
|
7
|
+
raise JsonLogic::InvalidArgumentsError.new unless items.is_a?(Array)
|
|
8
|
+
|
|
9
|
+
call_with_values(evaluate_values(items, rule, data))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
protected
|
|
13
|
+
|
|
14
|
+
def call_with_values(_values)
|
|
15
|
+
raise NotImplementedError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_items_and_rule(rules, data)
|
|
19
|
+
items_rule, rule = rules
|
|
20
|
+
items = JsonLogic.apply(items_rule, data)
|
|
21
|
+
[items, rule]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def evaluate_values(items, rule, data)
|
|
25
|
+
return [] if rule.nil?
|
|
26
|
+
|
|
27
|
+
items.each_with_index.map do |item, index|
|
|
28
|
+
JsonLogic.apply(rule, JsonLogic::Scope.new(item, data, index))
|
|
29
|
+
end
|
|
8
30
|
end
|
|
9
31
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsonLogic
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
DEFAULT_MESSAGE = "Error"
|
|
6
|
+
|
|
7
|
+
attr_reader :type, :message, :payload
|
|
8
|
+
|
|
9
|
+
def initialize(message = nil)
|
|
10
|
+
@message = message_or_default(message)
|
|
11
|
+
|
|
12
|
+
super(@message)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def payload
|
|
16
|
+
{ "type" => @message }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def message_or_default(message)
|
|
22
|
+
self.class < JsonLogic::Error || message.empty? ? default_message : message.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def default_message
|
|
26
|
+
self.class::DEFAULT_MESSAGE
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|