json-logic-rb 0.1.5 → 0.2.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +209 -169
  3. data/lib/json_logic/engine.rb +22 -21
  4. data/lib/json_logic/enumerable_operation.rb +27 -5
  5. data/lib/json_logic/errors/error.rb +29 -0
  6. data/lib/json_logic/errors/invalid_arguments_error.rb +7 -0
  7. data/lib/json_logic/errors/logic_error.rb +7 -0
  8. data/lib/json_logic/errors/nan_error.rb +7 -0
  9. data/lib/json_logic/ext/array.rb +5 -0
  10. data/lib/json_logic/operations/add.rb +3 -3
  11. data/lib/json_logic/operations/all.rb +3 -6
  12. data/lib/json_logic/operations/and.rb +6 -5
  13. data/lib/json_logic/operations/bool_cast.rb +2 -3
  14. data/lib/json_logic/operations/cat.rb +3 -1
  15. data/lib/json_logic/operations/coalesce.rb +9 -0
  16. data/lib/json_logic/operations/div.rb +7 -1
  17. data/lib/json_logic/operations/equal.rb +12 -3
  18. data/lib/json_logic/operations/exists.rb +14 -0
  19. data/lib/json_logic/operations/filter.rb +11 -3
  20. data/lib/json_logic/operations/gt.rb +12 -4
  21. data/lib/json_logic/operations/gte.rb +12 -4
  22. data/lib/json_logic/operations/if.rb +2 -0
  23. data/lib/json_logic/operations/in.rb +2 -0
  24. data/lib/json_logic/operations/lt.rb +10 -4
  25. data/lib/json_logic/operations/lte.rb +12 -4
  26. data/lib/json_logic/operations/map.rb +14 -2
  27. data/lib/json_logic/operations/max.rb +3 -1
  28. data/lib/json_logic/operations/merge.rb +4 -3
  29. data/lib/json_logic/operations/min.rb +3 -1
  30. data/lib/json_logic/operations/missing.rb +2 -26
  31. data/lib/json_logic/operations/missing_some.rb +6 -20
  32. data/lib/json_logic/operations/mod.rb +9 -1
  33. data/lib/json_logic/operations/mul.rb +4 -1
  34. data/lib/json_logic/operations/none.rb +4 -5
  35. data/lib/json_logic/operations/not_equal.rb +12 -3
  36. data/lib/json_logic/operations/or.rb +5 -1
  37. data/lib/json_logic/operations/preserve.rb +9 -0
  38. data/lib/json_logic/operations/reduce.rb +21 -5
  39. data/lib/json_logic/operations/some.rb +4 -7
  40. data/lib/json_logic/operations/strict_equal.rb +18 -3
  41. data/lib/json_logic/operations/strict_not_equal.rb +16 -3
  42. data/lib/json_logic/operations/sub.rb +8 -1
  43. data/lib/json_logic/operations/substr.rb +12 -20
  44. data/lib/json_logic/operations/ternary.rb +1 -7
  45. data/lib/json_logic/operations/throw.rb +12 -0
  46. data/lib/json_logic/operations/try.rb +35 -0
  47. data/lib/json_logic/operations/val.rb +79 -0
  48. data/lib/json_logic/operations/var.rb +22 -20
  49. data/lib/json_logic/scope.rb +67 -0
  50. data/lib/json_logic/semantics.rb +107 -38
  51. data/lib/json_logic/tree.rb +97 -0
  52. data/lib/json_logic/version.rb +1 -1
  53. data/lib/json_logic.rb +12 -0
  54. data/script/build_tests_json.rb +26 -0
  55. data/script/compliance.rb +160 -37
  56. data/spec/tmp/v2/tests.json +16981 -0
  57. metadata +22 -11
  58. /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: c8a44ea9eaa8d259e8122202d3db3608b8e6513f452f7daa0e1b53b96dbb3d09
4
- data.tar.gz: 29cc6846d185232738182e1d0e677d852e9efd8446bc31613c563817dac7d221
3
+ metadata.gz: fe25d64cef43ddb57ad9f94bec45b401ce7ee19b29757108336644342f75f8e1
4
+ data.tar.gz: 21e8ef8421a8768c8be32795c855cc52ec4a4afed79a5875399372417fa06da9
5
5
  SHA512:
6
- metadata.gz: 9bf0840e39f08f3e8c4a99a49196cace077ba17e005634df74349a58cf70b92e40e24907149fe00f9fb5b0c634ea0ad1f6deef222c0576b1f9224b3683162a5a
7
- data.tar.gz: a96a9d99cb366dd0ff07a974da4dce757aa1836e4abb553c80ea9caadbd28c96be5080063bff5749949b7fdc7b74ecf2feee7bf898093121bedb1a0299b8dfb5
6
+ metadata.gz: e228a5ed37724d97e54c87e726f772e5f5a749a114a8b02a332bf9131da06c83fedaf0f5d059bd61c94c1c0be73c34ab9fc1cd9d6c6408c958159d951c3a36c9
7
+ data.tar.gz: 805c23fe8af646123f4c5535111d14576ebbf6c1aeeaf6b01247bbc0dd92399e9584ddd869b4522b6a5734c601e701e7736b27b628ccc2cc576f7a26b211fe84
data/README.md CHANGED
@@ -1,9 +1,20 @@
1
1
 
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
2
13
  # json-logic-rb
3
14
 
4
- Ruby implementation of [JsonLogic](https://jsonlogic.com/) — simple and extensible. Ships with a compliance runner for the official test suite.
15
+ Ruby implementation of [JsonLogic](https://jsonlogic.com/) — elegant and extensible. Full compliance with both core and community-extended specifications.
5
16
 
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>
17
+ [![jsonlogic core][src-core]](https://jsonlogic.com/tests.json) [![jsonlogic community][src-community]](https://github.com/json-logic/compat-tables/tree/main/suites) [![compliance 100%](https://img.shields.io/badge/compliance-100%25-brightgreen)](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>
7
18
 
8
19
  ## Table of Contents
9
20
  - [What](#what)
@@ -12,10 +23,18 @@ Ruby implementation of [JsonLogic](https://jsonlogic.com/) — simple and extens
12
23
  - [How](#how)
13
24
  - [1. Default Operations](#1-default-operations)
14
25
  - [2. Lazy Operations](#2-lazy-operations)
26
+ - [Why laziness matters?](#why-laziness-matters)
27
+ - [Compliance and tests](#compliance-and-tests)
28
+ - [Script](#script)
15
29
  - [Supported Operations (Built‑in)](#supported-operations-built-in)
16
30
  - [Adding Operations](#adding-operations)
31
+ - [Enable JsonLogic Semantics (optional)](#enable-jsonlogic-semantics-optional)
32
+ - [Parameters](#parameters)
33
+ - [Proc / Lambda](#proc--lambda)
34
+ - [Class](#class)
17
35
  - [JsonLogic Semantic](#jsonlogic-semantic)
18
- - [Compliance and tests](#compliance-and-tests)
36
+ - [Comparisons](#comparisons)
37
+ - [Truthiness](#truthiness)
19
38
  - [Security](#security)
20
39
  - [License](#license)
21
40
  - [Authors](#authors)
@@ -27,9 +46,20 @@ JsonLogic rules are JSON trees. The engine walks that tree and returns a Ruby va
27
46
 
28
47
  ## Install
29
48
 
49
+ Download the gem locally
30
50
  ```bash
31
51
  gem install json-logic-rb
32
52
  ```
53
+ If needed – add to your Gemfile
54
+
55
+ ```ruby
56
+ gem "json-logic-rb"
57
+ ```
58
+
59
+ Then install
60
+ ```shell
61
+ bundle install
62
+ ```
33
63
 
34
64
  ## Quick start
35
65
 
@@ -45,42 +75,39 @@ JsonLogic.apply(rule)
45
75
  With data:
46
76
 
47
77
  ```ruby
48
- JsonLogic.apply({ "var" => "user.age" }, { "user" => { "age" => 42 } })
78
+ require 'json_logic'
79
+
80
+ rule = { "var" => "user.age" }
81
+ data = { "user" => { "age" => 42 } }
82
+
83
+ JsonLogic.apply(rule, data)
49
84
  # => 42
50
85
  ```
51
86
 
52
87
  ## How
53
88
 
54
- There are **two types of operations** in this implementation: Default Operations and Lazy Operations.
89
+ There are two types of operations: [Default Operations](#1-default-operations) and [Lazy Operations](#2-lazy-operations).
55
90
 
56
91
  ### 1. Default Operations
57
92
 
58
- For **Default Operations**, the engine **evaluates all arguments first** and then calls the operator with the **resulting Ruby values**.
93
+ For **Default Operations**, the it evaluates all arguments first and then calls the operator with the resulting Ruby values.
59
94
  This matches the reference behavior for arithmetic, comparisons, string operations, and other pure operations that do not control evaluation order.
60
95
 
61
96
  **Groups and references:**
62
97
 
63
98
  - [Numeric operations](https://jsonlogic.com/operations.html#numeric-operations)
64
99
  - [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`.
100
+ - [Array operations](https://jsonlogic.com/operations.html#array-operations) — simple transform like `merge`.
66
101
 
67
102
  ### 2. Lazy Operations
68
103
 
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.
104
+ 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.
70
105
 
71
106
  **Groups and references:**
72
107
 
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)
75
-
76
- - **Enumerable operators** — `map`, `filter`, `reduce`, `all`, `none`, `some`
77
- [Array operations](https://jsonlogic.com/operations.html#array-operations)
78
-
79
- **How enumerable per-item evaluation works:**
80
-
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"`.
108
+ - [Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations)short-circuit/branching like `or`.
109
+ - [Comparison operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) equality/ordering like `==`.
110
+ - [Array operations](https://jsonlogic.com/operations.html#array-operations) — enumerable evaluation like `map`.
84
111
 
85
112
 
86
113
  **Example #1**
@@ -94,168 +121,214 @@ JsonLogic.apply(
94
121
  # => [2, 3]
95
122
  ```
96
123
 
97
- **Example #2**
98
-
99
- ```ruby
100
- # reduce: sum using "current" and "accumulator"
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
109
- ```
110
-
111
124
  ### Why laziness matters?
112
125
 
113
- Lazy operations **prevent evaluation** of branches you do not need. If division by zero raised an error (hypothetically), lazy control would avoid it:
126
+ Lazy operations prevent evaluation of branches you do not need.
114
127
 
128
+ If hypothetically division by zero raises an error, lazy control would avoid it.
115
129
  ```ruby
116
- # "or" short-circuits: 1 is truthy, so the right side is NOT evaluated.
117
- # If the right side were evaluated eagerly, it would attempt 1/0 (error).
118
130
  JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
119
131
  # => 1
120
132
  ```
121
133
 
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.
134
+ > 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.
123
135
 
124
- ## Supported Operations (Built‑in)
125
136
 
126
137
 
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
138
 
170
- ## Adding Operations
171
139
 
172
- Need a custom operation? It’s straightforward.
173
140
 
174
- ### Quick register a Proc or Lambda
141
+ ## Compliance and tests
175
142
 
176
- Register little anonymous functions, by passing a Proc or Lambda.
143
+ The JsonLogic specification provides test suites concrete inputs with expected outputs that validate the implementation of operations. The specification come in two variants:
144
+ - [![jsonlogic core][src-core]](https://jsonlogic.com/tests.json) — [original JsonLogic website](https://jsonlogic.com/tests.json);
145
+ - [![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);
177
146
 
178
- ```ruby
179
- JsonLogic.add_operation("times2") { |(value), _| value.to_i * 2 }
180
- ```
147
+ 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.
181
148
 
182
- Once the function added, you can use it in your logic.
149
+ ### Script
183
150
 
184
- ```ruby
185
- JsonLogic.apply({ "times2" => [21] })
186
- # => 42
187
- ```
151
+ Download test suite v1:
188
152
 
189
- Is useful for rapid prototyping with minimal boilerplate;
190
- Later you can “promote” it into a full class or use additional features.
153
+ ```bash
154
+ mkdir -p spec/tmp/v1
155
+ curl -L https://jsonlogic.com/tests.json -o spec/tmp/v1/tests.json
156
+ ```
191
157
 
158
+ Download test suite v2:
192
159
 
193
- ### 1) Pick the Operation type
194
- Choose one of:
195
- - **Default**
196
- ```ruby
197
- class JsonLogic::Operations::StartsWith < JsonLogic::Operation; end
160
+ ```bash
161
+ mkdir -p spec/tmp/v2
162
+ git clone https://github.com/json-logic/compat-tables.git /tmp/compat-tables
163
+ ruby script/build_tests_json.rb /tmp/compat-tables/suites spec/tmp/v2/tests.json
198
164
  ```
199
- For anonymous functions:
200
- ```ruby
201
- JsonLogic.add_operation("starts_with", lazy: false) do; end
165
+
166
+ Run by version:
167
+
168
+ ```bash
169
+ ruby script/compliance.rb -v 1
170
+ ruby script/compliance.rb -v 2
202
171
  ```
203
- - **Lazy**
204
- ```ruby
205
- class JsonLogic::Operations::StartsWith < JsonLogic::LazyOperation; end
172
+
173
+ Run by file path:
174
+
175
+ ```bash
176
+ ruby script/compliance.rb -f spec/tmp/v2/tests.json
206
177
  ```
207
- For anonymous functions:
178
+
179
+
180
+ ## Supported Operations (Built‑in)
181
+
182
+ Don’t expect JsonLogic to include every specialized operation. It’s intentionally small and not a programming language. It will never do everything.
183
+
184
+ You can add custom operations yourself — check out [§Adding Operations](https://www.google.com/search?q=%23adding-operations)— or consider if the logic can be expressed with what’s already there.
185
+
186
+ If a feature is simple, lightweight, and universally needed — open an issue or discussion.
187
+
188
+
189
+ | Operator | Supported | Source |
190
+ |---|---:|---|
191
+ | [Data / Presence](https://jsonlogic.com/operations.html#accessing-data) | | |
192
+ | `var` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
193
+ | `val` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
194
+ | `missing` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
195
+ | `missing_some` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
196
+ | `exists` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
197
+ | [Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) | | |
198
+ | `if` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
199
+ | `?:` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
200
+ | `and` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
201
+ | `or` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
202
+ | `!` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
203
+ | `!!` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
204
+ | [Comparison Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) | | |
205
+ | `==` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
206
+ | `===` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
207
+ | `!=` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
208
+ | `!==` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
209
+ | `>` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
210
+ | `>=` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
211
+ | `<` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
212
+ | `<=` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
213
+ | [Numeric Operations](https://jsonlogic.com/operations.html#numeric-operations) | | |
214
+ | `+` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
215
+ | `-` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
216
+ | `*` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
217
+ | `/` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
218
+ | `%` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
219
+ | `min` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
220
+ | `max` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
221
+ | [Array Operations](https://jsonlogic.com/operations.html#array-operations) | | |
222
+ | `map` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
223
+ | `reduce` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
224
+ | `filter` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
225
+ | `all` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
226
+ | `none` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
227
+ | `some` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
228
+ | `merge` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
229
+ | [String Operations](https://jsonlogic.com/operations.html#string-operations) | | |
230
+ | `in` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
231
+ | `cat` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
232
+ | `substr` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
233
+ | [Community Extensions](https://github.com/json-logic/compat-tables/tree/main/suites) | | |
234
+ | `??` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
235
+ | `try` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
236
+ | `throw` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
237
+ | `preserve` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
238
+ | Docs-only / Not implemented | | |
239
+ | `log` | 🚫 | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
240
+
241
+
242
+ [src-core]: https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square
243
+ [src-community]: https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square
244
+
245
+ ## Adding Operations
246
+
247
+ Need a custom Operation? It’s straightforward. Start small with a Proc or Lambda. If needed – promote it to a Class.
248
+
249
+
250
+
251
+ ### Enable JsonLogic Semantics (optional)
252
+ Enable semantics to mirror JsonLogic’s comparison and truthiness in Ruby.
253
+
254
+ See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
255
+
256
+
257
+ ### Parameters
258
+
259
+ Operator function use a consistent call shape:
260
+
261
+ - First parameter: **array of operator arguments** (you can destructure it).
262
+
263
+ - Second parameter: current **data**.
208
264
  ```ruby
209
- JsonLogic.add_operation("starts_with", lazy: true) do; end
265
+ ->((string, prefix), data) { string.to_s.start_with?(prefix.to_s) }
210
266
  ```
211
267
 
212
- See [§How](#how) for details.
268
+ ### Proc / Lambda
269
+
270
+ Pick the Operation type.
213
271
 
214
- ### 2) Enable JsonLogic Semantics (optional)
215
- Enable semantics to mirror JsonLogic’s comparison/truthiness in Ruby:
272
+ [Default Operation](#1-default-operations) mode passes values.
216
273
 
217
274
  ```ruby
218
- using JsonLogic::Semantics
275
+ JsonLogic.add_operation("starts_with") do |(string_value, prefix_value), _data|
276
+ string_value.to_s.start_with?(prefix_value.to_s)
277
+ end
219
278
  ```
279
+ [Lazy Operation](#2-lazy-operations) mode passes raw rules (you evaluate them):
220
280
 
281
+ ```ruby
282
+ JsonLogic.add_operation("starts_with", lazy: true) do |(string_rule, prefix_rule), data|
283
+ string_value = JsonLogic.apply(string_rule, data)
284
+ prefix_value = JsonLogic.apply(prefix_rule, data)
285
+ string_value.to_s.start_with?(prefix_value.to_s)
286
+ end
287
+ ```
221
288
 
222
- See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
289
+ See [§How](https://github.com/tavrelkate/json-logic-rb?tab=readme-ov-file#how) for details.
223
290
 
224
- ### 3) Create an Operation and provide a machine name
291
+ Use immediately:
225
292
 
226
- Operation methods use a consistent call shape.
293
+ ```ruby
294
+ JsonLogic.apply({ "starts_with" => [ { "var" => "email" }, "admin@" ] })
295
+ ```
227
296
 
228
- - The first parameter is the **array of operator arguments**.
229
- - The second is the current **data**.
230
297
 
298
+ ### Class
231
299
 
300
+ Pick the Operation type. It has the same call shape.
232
301
 
233
- Thanks to Ruby’s destructuring, you can unpack the argument array right in the method signature.
302
+ [Default Operation](#1-default-operations) – Inherit `JsonLogic::Operation`.
234
303
 
235
304
  ```ruby
236
305
  class JsonLogic::Operations::StartsWith < JsonLogic::Operation
237
306
  def self.name = "starts_with"
238
- def call((str, prefix), _data)
239
- # str, prefix are ALREADY evaluated to Ruby values
240
- str.to_s.start_with?(prefix.to_s)
241
- end
307
+ def call(string_value, prefix_value), _data) = string_value.to_s.start_with?(prefix_value.to_s)
242
308
  end
243
309
  ```
244
310
 
245
- ### 4) Register the new operation
311
+ [Lazy Operation](#2-lazy-operations) Inherit `JsonLogic::LazyOperation`.
312
+
313
+ Register explicitly:
246
314
 
247
315
  ```ruby
248
316
  JsonLogic::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
249
317
  ```
250
318
 
251
- After registration, use it in rules:
319
+ Now, Class is ready to use.
252
320
 
253
- ```json
254
- { "starts_with": [ { "var": "email" }, "admin@" ] }
321
+ ```ruby
322
+ JsonLogic.apply({ "starts_with" => [ { "var" => "email" }, "admin@" ] })
255
323
  ```
256
324
 
257
325
 
258
326
 
327
+
328
+
329
+
330
+
331
+
259
332
  ## JsonLogic Semantic
260
333
 
261
334
  All supported Operations follow JsonLogic semantics.
@@ -281,13 +354,14 @@ As JsonLogic primary developed in JavaScript it inherits JavaScript's type coerc
281
354
  ```ruby
282
355
  using JsonLogic::Semantics
283
356
 
284
- 1 >= "1.0" # => true
357
+ 1 >= "1.0"
358
+ # => true
285
359
  ```
286
360
 
287
361
  ### Truthiness
288
362
 
289
- JsonLogic’s truthiness differs from Ruby’s (see <https://jsonlogic.com/truthy.html>).
290
- In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are also falsey.
363
+ JsonLogic’s truthiness differs from Ruby’s (see [Json Logic Website Truthy and Falsy](https://jsonlogic.com/truthy.html)).
364
+ In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty arrays are falsey too.
291
365
 
292
366
  **In Ruby:**
293
367
  ```ruby
@@ -295,57 +369,23 @@ In Ruby, only `false` and `nil` are falsey. In JsonLogic empty strings and empty
295
369
  # => true
296
370
  ```
297
371
 
298
- While JsonLogic as was mentioned before has it's own truthiness:
372
+ While JsonLogic as was mentioned before has it's own truthiness.
299
373
 
300
374
  **In Ruby (with JsonLogic Semantic):**
301
375
 
302
376
  ```ruby
303
- include JsonLogic::Semantics
377
+ using JsonLogic::Semantics
304
378
 
305
- truthy?([])
379
+ !![]
306
380
  # => false
307
381
  ```
308
382
 
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
383
  ## Security
345
384
 
346
- - Rules are **data**, not code; no Ruby eval.
347
- - Operations are **pure** (no IO, no network, no shell).
348
- - Rules have **no write** access to anything.
385
+ - RULES ARE DATA; NO RUBY EVAL;
386
+ - OPERATIONS ARE PURE; NO IO, NO NETWORK; NO SHELL;
387
+ - RULES HAVE NO WRITE ACCESS TO ANYTHING;
388
+
349
389
 
350
390
  ## License
351
391
 
@@ -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
- 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)
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
- private
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
- 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]
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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonLogic
4
+ class InvalidArgumentsError < Error
5
+ DEFAULT_MESSAGE = "Invalid Arguments"
6
+ end
7
+ end