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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +197 -197
  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 +4 -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 +26 -3
  41. data/lib/json_logic/operations/strict_not_equal.rb +24 -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 +24 -13
  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: bf7e4291a1fe0a56b1387ca35f03d239679f4d0c76333a3d0d0f3933efeead0b
4
+ data.tar.gz: 134bb254ff81c389daa1d9527b9496d17d0417316143586ea8ade75c739f2657
5
5
  SHA512:
6
- metadata.gz: 9bf0840e39f08f3e8c4a99a49196cace077ba17e005634df74349a58cf70b92e40e24907149fe00f9fb5b0c634ea0ad1f6deef222c0576b1f9224b3683162a5a
7
- data.tar.gz: a96a9d99cb366dd0ff07a974da4dce757aa1836e4abb553c80ea9caadbd28c96be5080063bff5749949b7fdc7b74ecf2feee7bf898093121bedb1a0299b8dfb5
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) [![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>
1
2
 
2
3
  # json-logic-rb
3
4
 
4
- Ruby implementation of [JsonLogic](https://jsonlogic.com/) simple and extensible. Ships with a compliance runner for the official test suite.
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
- - [How](#how)
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
- - [Supported Operations (Built‑in)](#supported-operations-built-in)
16
- - [Adding Operations](#adding-operations)
21
+ - [Why laziness matters?](#why-laziness-matters)
17
22
  - [JsonLogic Semantic](#jsonlogic-semantic)
18
- - [Compliance and tests](#compliance-and-tests)
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
- JsonLogic.apply({ "var" => "user.age" }, { "user" => { "age" => 42 } })
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
- **Groups and references:**
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` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
96
+ | `val` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
97
+ | `missing` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
98
+ | `missing_some` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
99
+ | `exists` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
100
+ | [Logic and Boolean Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) | | |
101
+ | `if` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
102
+ | `?:` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
103
+ | `and` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
104
+ | `or` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
105
+ | `!` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
106
+ | `!!` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
107
+ | [Comparison Operations](https://jsonlogic.com/operations.html#logic-and-boolean-operations) | | |
108
+ | `==` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
109
+ | `===` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
110
+ | `!=` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
111
+ | `!==` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
112
+ | `>` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
113
+ | `>=` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
114
+ | `<` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
115
+ | `<=` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
116
+ | [Numeric Operations](https://jsonlogic.com/operations.html#numeric-operations) | | |
117
+ | `+` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
118
+ | `-` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
119
+ | `*` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
120
+ | `/` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
121
+ | `%` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
122
+ | `min` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
123
+ | `max` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
124
+ | [Array Operations](https://jsonlogic.com/operations.html#array-operations) | | |
125
+ | `map` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
126
+ | `reduce` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
127
+ | `filter` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
128
+ | `all` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
129
+ | `none` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
130
+ | `some` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
131
+ | `merge` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
132
+ | [String Operations](https://jsonlogic.com/operations.html#string-operations) | | |
133
+ | `in` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
134
+ | `cat` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
135
+ | `substr` | ✅ | ![jsonlogic](https://img.shields.io/badge/jsonlogic-core-2ea44f?style=flat-square) |
136
+ | [Community Extensions](https://github.com/json-logic/compat-tables/tree/main/suites) | | |
137
+ | `??` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
138
+ | `try` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
139
+ | `throw` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
140
+ | `preserve` | ✅ | ![jsonlogic community](https://img.shields.io/badge/jsonlogic--community-extra-0366d6?style=flat-square) |
141
+ | Docs-only / Not implemented | | |
142
+ | `log` | 🚫 | ![jsonlogic docs](https://img.shields.io/badge/jsonlogic-docs-6a737d?style=flat-square) |
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
- ### 2. Lazy Operations
149
+ ## Adding Operations
68
150
 
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.
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
- **Groups and references:**
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
- - **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)
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
- **How enumerable per-item evaluation works:**
158
+ ### Enable JsonLogic Semantics (optional)
159
+ Enable semantics to mirror JsonLogic’s comparison and truthiness in Ruby.
80
160
 
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"`.
161
+ See [§JsonLogic Semantic](#jsonlogic-semantic) for details.
84
162
 
85
163
 
86
- **Example #1**
164
+ ### Parameters
87
165
 
88
- ```ruby
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
- **Example #2**
168
+ - First parameter: **array of operator arguments** (you can destructure it).
98
169
 
170
+ - Second parameter: current **data**.
99
171
  ```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
172
+ ->((string, prefix), data) { string.to_s.start_with?(prefix.to_s) }
109
173
  ```
110
174
 
111
- ### Why laziness matters?
175
+ ### Proc / Lambda
176
+
177
+ Pick the Operation type.
112
178
 
113
- Lazy operations **prevent evaluation** of branches you do not need. If division by zero raised an error (hypothetically), lazy control would avoid it:
179
+ [Default Operation](#1-default-operations) mode passes values.
114
180
 
115
181
  ```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
- JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
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
- > 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.
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
- ### Quick register a Proc or Lambda
196
+ See [§Laziness](#laziness) for details.
175
197
 
176
- Register little anonymous functions, by passing a Proc or Lambda.
198
+ Use immediately:
177
199
 
178
200
  ```ruby
179
- JsonLogic.add_operation("times2") { |(value), _| value.to_i * 2 }
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
- ```ruby
185
- JsonLogic.apply({ "times2" => [21] })
186
- # => 42
187
- ```
205
+ ### Class
188
206
 
189
- Is useful for rapid prototyping with minimal boilerplate;
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.add_operation("starts_with", lazy: false) do; end
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
- - **Lazy**
217
+
218
+ [Lazy Operation](#2-lazy-operations) – Inherit `JsonLogic::LazyOperation`.
219
+
220
+ Register explicitly:
221
+
204
222
  ```ruby
205
- class JsonLogic::Operations::StartsWith < JsonLogic::LazyOperation; end
223
+ JsonLogic::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
206
224
  ```
207
- For anonymous functions:
225
+
226
+ Now, Class is ready to use.
227
+
208
228
  ```ruby
209
- JsonLogic.add_operation("starts_with", lazy: true) do; end
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
- Thanks to Ruby’s destructuring, you can unpack the argument array right in the method signature.
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
- class JsonLogic::Operations::StartsWith < JsonLogic::Operation
237
- 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
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
- ### 4) Register the new operation
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::Engine.default.registry.register(JsonLogic::Operations::StartsWith)
283
+ JsonLogic.apply({ "or" => [1, { "/" => [1, 0] }] })
284
+ # => 1
249
285
  ```
250
286
 
251
- After registration, use it in rules:
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" # => true
317
+ 1 >= "1.0"
318
+ # => true
285
319
  ```
286
320
 
287
321
  ### Truthiness
288
322
 
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.
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
- include JsonLogic::Semantics
337
+ using JsonLogic::Semantics
304
338
 
305
- truthy?([])
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
- - 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.
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
- ## Authors
354
+ ## Maintainers
355
355
 
356
- - [Valeriya Petrova](https://github.com/piatrova-valeriya1999)
356
+ - [Valeriya Petrova](https://github.com/valeryia-piatrova)
357
357
  - [Tavrel Kate](https://github.com/tavrelkate)
@@ -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