shiny_json_logic 0.3.5 → 0.3.6
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/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/lib/shiny_json_logic/engine.rb +12 -5
- data/lib/shiny_json_logic/numericals/min_max_collection.rb +21 -13
- data/lib/shiny_json_logic/operations/addition.rb +6 -2
- data/lib/shiny_json_logic/operations/and.rb +5 -2
- data/lib/shiny_json_logic/operations/base.rb +8 -4
- data/lib/shiny_json_logic/operations/coalesce.rb +5 -2
- data/lib/shiny_json_logic/operations/concatenation.rb +18 -5
- data/lib/shiny_json_logic/operations/division.rb +5 -2
- data/lib/shiny_json_logic/operations/exists.rb +8 -8
- data/lib/shiny_json_logic/operations/filter.rb +11 -5
- data/lib/shiny_json_logic/operations/inclusion.rb +8 -1
- data/lib/shiny_json_logic/operations/iterable/base.rb +23 -22
- data/lib/shiny_json_logic/operations/max.rb +1 -1
- data/lib/shiny_json_logic/operations/merge.rb +14 -3
- data/lib/shiny_json_logic/operations/min.rb +1 -1
- data/lib/shiny_json_logic/operations/missing.rb +34 -13
- data/lib/shiny_json_logic/operations/missing_some.rb +21 -5
- data/lib/shiny_json_logic/operations/modulo.rb +5 -2
- data/lib/shiny_json_logic/operations/or.rb +5 -2
- data/lib/shiny_json_logic/operations/preserve.rb +6 -4
- data/lib/shiny_json_logic/operations/product.rb +5 -2
- data/lib/shiny_json_logic/operations/reduce.rb +13 -11
- data/lib/shiny_json_logic/operations/subtraction.rb +5 -2
- data/lib/shiny_json_logic/operations/throw.rb +1 -1
- data/lib/shiny_json_logic/operations/try.rb +7 -3
- data/lib/shiny_json_logic/operations/val.rb +29 -8
- data/lib/shiny_json_logic/operations/var.rb +14 -8
- data/lib/shiny_json_logic/operator_solver.rb +1 -1
- data/lib/shiny_json_logic/scope_stack.rb +23 -48
- data/lib/shiny_json_logic/truthy.rb +10 -2
- data/lib/shiny_json_logic/utils/array.rb +0 -3
- data/lib/shiny_json_logic/version.rb +1 -1
- data/lib/shiny_json_logic.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: cb0d5385d70e0de02275f1a5a4ad0f8d6014c9f8030df64882cb7da1ff44942d
|
|
4
|
+
data.tar.gz: a608c50e2ef5acb46a348674df1ffeff928274f142f5d71ffbaf1101787b4149
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eef55803823c85357152265d6b9050394151111b782c233c264857837a35e822149c4f93c792fd1f79b9917c1c6956c7237949dd11f5ba057908b9b0a31372ae
|
|
7
|
+
data.tar.gz: 2dc4f7ee486216838700097498ff5b545307780ba274fd869a4c40accd46799f1111dcdeb12b55518926de4bce1339c57bdf5d42ba581fc3e17c452aa5e42aa1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [0.3.6] - 2026-03-06
|
|
5
|
+
### Changed
|
|
6
|
+
- Optimizes instantiation of scope stack for improved performance.
|
|
7
|
+
- Refactors min/max operations for improved performance.
|
|
8
|
+
|
|
4
9
|
## [0.3.5] - 2026-03-06
|
|
5
10
|
### Changed
|
|
6
11
|
- Reduces object allocations further in hot paths (iterator index tracking, early-exit via throw/catch, inline nil-wrapping, scope push simplification) for an additional ~12–18% throughput improvement over 0.3.4.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
[](https://badge.fury.io/rb/shiny_json_logic)
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
> **A boring, correct and production-ready JSONLogic implementation for Ruby. ✨**
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ This gem focuses on predictable behavior, strict spec alignment, high compatibil
|
|
|
15
15
|
## Why ShinyJsonLogic?
|
|
16
16
|
|
|
17
17
|
- 🧩 **Zero runtime dependencies** (stdlib-only). Just plug & play!
|
|
18
|
-
- 🕰️ **Ruby 2.
|
|
18
|
+
- 🕰️ **Ruby 2.4+ compatible**, one of the lowest minimum versions supported in the Ruby ecosystem.
|
|
19
19
|
- 🔧 **Actively maintained** and continuously improved.
|
|
20
20
|
- 📊 **Highest JSONLogic compatibility in the Ruby ecosystem**, as measured against the official test suites.
|
|
21
21
|
|
|
@@ -8,10 +8,9 @@ module ShinyJsonLogic
|
|
|
8
8
|
OPERATIONS = OperatorSolver::SOLVERS
|
|
9
9
|
|
|
10
10
|
def self.call(rule, scope_stack)
|
|
11
|
-
if rule.is_a?(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return rule if rule.empty?
|
|
11
|
+
if rule.is_a?(Hash)
|
|
12
|
+
# DataHash marks already-resolved user data — return as-is without dispatch
|
|
13
|
+
return rule if rule.is_a?(Utils::DataHash) || rule.empty?
|
|
15
14
|
|
|
16
15
|
raise Errors::UnknownOperator if rule.size > 1
|
|
17
16
|
|
|
@@ -24,7 +23,15 @@ module ShinyJsonLogic
|
|
|
24
23
|
|
|
25
24
|
op.call(args, scope_stack)
|
|
26
25
|
elsif rule.is_a?(Array)
|
|
27
|
-
|
|
26
|
+
# Use while loop instead of map — avoids Enumerator overhead on Ruby 3.4 no-YJIT
|
|
27
|
+
n = rule.size
|
|
28
|
+
result = Array.new(n)
|
|
29
|
+
i = 0
|
|
30
|
+
while i < n
|
|
31
|
+
result[i] = call(rule[i], scope_stack)
|
|
32
|
+
i += 1
|
|
33
|
+
end
|
|
34
|
+
result
|
|
28
35
|
else
|
|
29
36
|
rule
|
|
30
37
|
end
|
|
@@ -7,21 +7,29 @@ module ShinyJsonLogic
|
|
|
7
7
|
module MinMaxCollection
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def collect_values(rules, scope_stack)
|
|
18
|
-
if Operations::Base.op?(rules)
|
|
19
|
-
evaluated = Engine.call(rules, scope_stack)
|
|
20
|
-
return Utils::Array.wrap_nil(evaluated)
|
|
10
|
+
def resolve(rules, scope_stack, op)
|
|
11
|
+
if rules.is_a?(Hash) && !rules.empty? && Engine::OPERATIONS.key?(rules.first[0].to_s)
|
|
12
|
+
items = Utils::Array.wrap_nil(Engine.call(rules, scope_stack))
|
|
13
|
+
evaluated = true
|
|
14
|
+
else
|
|
15
|
+
items = Utils::Array.wrap_nil(rules)
|
|
16
|
+
evaluated = false
|
|
21
17
|
end
|
|
22
18
|
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
raise Errors::InvalidArguments if items.empty?
|
|
20
|
+
|
|
21
|
+
best = evaluated ? items[0] : Engine.call(items[0], scope_stack)
|
|
22
|
+
raise Errors::InvalidArguments unless best.is_a?(Numeric)
|
|
23
|
+
|
|
24
|
+
i = 1
|
|
25
|
+
n = items.size
|
|
26
|
+
while i < n
|
|
27
|
+
v = evaluated ? items[i] : Engine.call(items[i], scope_stack)
|
|
28
|
+
raise Errors::InvalidArguments unless v.is_a?(Numeric)
|
|
29
|
+
best = v if op == :min ? v < best : v > best
|
|
30
|
+
i += 1
|
|
31
|
+
end
|
|
32
|
+
best
|
|
25
33
|
end
|
|
26
34
|
end
|
|
27
35
|
end
|
|
@@ -11,10 +11,14 @@ module ShinyJsonLogic
|
|
|
11
11
|
|
|
12
12
|
def self.execute(rules, scope_stack)
|
|
13
13
|
safe_arithmetic do
|
|
14
|
+
operands = Utils::Array.wrap_nil(rules)
|
|
14
15
|
result = 0.0
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
i = 0
|
|
17
|
+
n = operands.size
|
|
18
|
+
while i < n
|
|
19
|
+
val = Numericals::Numerify.numerify(evaluate(operands[i], scope_stack))
|
|
17
20
|
result += val.nil? ? 0 : val
|
|
21
|
+
i += 1
|
|
18
22
|
end
|
|
19
23
|
result
|
|
20
24
|
end
|
|
@@ -15,9 +15,12 @@ module ShinyJsonLogic
|
|
|
15
15
|
return false if rules.empty?
|
|
16
16
|
|
|
17
17
|
result = nil
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
i = 0
|
|
19
|
+
n = rules.size
|
|
20
|
+
while i < n
|
|
21
|
+
result = evaluate(rules[i], scope_stack)
|
|
20
22
|
return result unless Truthy.call(result)
|
|
23
|
+
i += 1
|
|
21
24
|
end
|
|
22
25
|
result
|
|
23
26
|
end
|
|
@@ -13,10 +13,14 @@ module ShinyJsonLogic
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def self.resolve_rules(rules, scope_stack)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
# Inline op? check to avoid extra method-call overhead on the hot path.
|
|
17
|
+
# Most rule args are primitives/arrays — the is_a?(Hash) guard is cheap.
|
|
18
|
+
if rules.is_a?(Hash) && !rules.empty? && !rules.is_a?(Utils::DataHash)
|
|
19
|
+
key = rules.first[0]
|
|
20
|
+
return rules unless Engine::OPERATIONS.key?(key.to_s)
|
|
21
|
+
raise Errors::InvalidArguments if raise_on_dynamic_args?
|
|
22
|
+
return Engine.call(rules, scope_stack)
|
|
23
|
+
end
|
|
20
24
|
rules
|
|
21
25
|
end
|
|
22
26
|
|
|
@@ -6,9 +6,12 @@ module ShinyJsonLogic
|
|
|
6
6
|
module Operations
|
|
7
7
|
class Coalesce < Base
|
|
8
8
|
def self.execute(rules, scope_stack)
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
i = 0
|
|
10
|
+
n = rules.size
|
|
11
|
+
while i < n
|
|
12
|
+
result = evaluate(rules[i], scope_stack)
|
|
11
13
|
return result unless result.nil?
|
|
14
|
+
i += 1
|
|
12
15
|
end
|
|
13
16
|
nil
|
|
14
17
|
end
|
|
@@ -6,12 +6,25 @@ module ShinyJsonLogic
|
|
|
6
6
|
module Operations
|
|
7
7
|
class Concatenation < Base
|
|
8
8
|
def self.execute(rules, scope_stack)
|
|
9
|
-
result =
|
|
10
|
-
Utils::Array.wrap_nil(rules)
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
result = +""
|
|
10
|
+
operands = Utils::Array.wrap_nil(rules)
|
|
11
|
+
i = 0
|
|
12
|
+
n = operands.size
|
|
13
|
+
while i < n
|
|
14
|
+
evaluated = evaluate(operands[i], scope_stack)
|
|
15
|
+
if evaluated.is_a?(Array)
|
|
16
|
+
j = 0
|
|
17
|
+
m = evaluated.size
|
|
18
|
+
while j < m
|
|
19
|
+
result << evaluated[j].to_s
|
|
20
|
+
j += 1
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
result << evaluated.to_s
|
|
24
|
+
end
|
|
25
|
+
i += 1
|
|
13
26
|
end
|
|
14
|
-
result
|
|
27
|
+
result
|
|
15
28
|
end
|
|
16
29
|
end
|
|
17
30
|
end
|
|
@@ -15,14 +15,17 @@ module ShinyJsonLogic
|
|
|
15
15
|
|
|
16
16
|
result = nil
|
|
17
17
|
count = 0
|
|
18
|
+
i = 0
|
|
19
|
+
n = operands.size
|
|
18
20
|
|
|
19
21
|
begin
|
|
20
|
-
|
|
21
|
-
evaluated = evaluate(
|
|
22
|
+
while i < n
|
|
23
|
+
evaluated = evaluate(operands[i], scope_stack)
|
|
22
24
|
num = Numericals::Numerify.numerify(evaluated)
|
|
23
25
|
return handle_nan if num.nil?
|
|
24
26
|
count += 1
|
|
25
27
|
result = result.nil? ? num : result / num
|
|
28
|
+
i += 1
|
|
26
29
|
end
|
|
27
30
|
rescue TypeError
|
|
28
31
|
return handle_nan
|
|
@@ -6,17 +6,17 @@ module ShinyJsonLogic
|
|
|
6
6
|
module Operations
|
|
7
7
|
class Exists < Base
|
|
8
8
|
def self.execute(rules, scope_stack)
|
|
9
|
-
current = scope_stack.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
current = scope_stack.last
|
|
10
|
+
operands = Utils::Array.wrap_nil(rules)
|
|
11
|
+
i = 0
|
|
12
|
+
n = operands.size
|
|
13
|
+
while i < n
|
|
14
|
+
segment = evaluate(operands[i], scope_stack)
|
|
15
|
+
return false unless current.is_a?(Hash) && current.key?(segment)
|
|
14
16
|
current = current[segment]
|
|
17
|
+
i += 1
|
|
15
18
|
end
|
|
16
|
-
|
|
17
19
|
true
|
|
18
|
-
rescue StandardError
|
|
19
|
-
false
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -11,17 +11,23 @@ module ShinyJsonLogic
|
|
|
11
11
|
|
|
12
12
|
def self.call(rules, scope_stack)
|
|
13
13
|
rules = resolve_rules(rules, scope_stack)
|
|
14
|
+
filter = setup_filter(rules)
|
|
15
|
+
collection = setup_collection(rules, scope_stack)
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
collection.
|
|
18
|
-
|
|
17
|
+
result = []
|
|
18
|
+
i = 0
|
|
19
|
+
n = collection.size
|
|
20
|
+
while i < n
|
|
21
|
+
item = collection[i]
|
|
22
|
+
scope_stack << item
|
|
19
23
|
begin
|
|
20
|
-
|
|
24
|
+
result << item if Truthy.call(Engine.call(filter, scope_stack))
|
|
21
25
|
ensure
|
|
22
26
|
scope_stack.pop
|
|
23
27
|
end
|
|
28
|
+
i += 1
|
|
24
29
|
end
|
|
30
|
+
result
|
|
25
31
|
end
|
|
26
32
|
end
|
|
27
33
|
end
|
|
@@ -13,7 +13,14 @@ module ShinyJsonLogic
|
|
|
13
13
|
needle = needle.to_s if needle.is_a?(Symbol)
|
|
14
14
|
|
|
15
15
|
if haystack.is_a?(Array)
|
|
16
|
-
|
|
16
|
+
i = 0
|
|
17
|
+
n = haystack.size
|
|
18
|
+
while i < n
|
|
19
|
+
el = haystack[i]
|
|
20
|
+
return true if (el.is_a?(Symbol) ? el.to_s : el) == needle
|
|
21
|
+
i += 1
|
|
22
|
+
end
|
|
23
|
+
false
|
|
17
24
|
else
|
|
18
25
|
haystack.include?(needle)
|
|
19
26
|
end
|
|
@@ -14,38 +14,43 @@ module ShinyJsonLogic
|
|
|
14
14
|
@raise_on_nil_filter
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def self.setup_filter(rules)
|
|
18
|
+
raise Errors::InvalidArguments unless rules.is_a?(Array)
|
|
19
|
+
filter = rules[1]
|
|
20
|
+
raise Errors::InvalidArguments if filter.nil? && raise_on_nil_filter?
|
|
21
|
+
filter
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.setup_collection(rules, scope_stack)
|
|
25
|
+
collection_rule = rules.size > 0 ? rules[0] : rules
|
|
26
|
+
raise Errors::InvalidArguments if collection_rule.nil?
|
|
27
|
+
Utils::Array.wrap(Engine.call(collection_rule, scope_stack))
|
|
28
|
+
end
|
|
29
|
+
|
|
17
30
|
def self.call(rules, scope_stack)
|
|
18
31
|
rules = resolve_rules(rules, scope_stack)
|
|
19
|
-
|
|
20
|
-
collection
|
|
32
|
+
filter = setup_filter(rules)
|
|
33
|
+
collection = setup_collection(rules, scope_stack)
|
|
21
34
|
|
|
22
35
|
early = catch(:early_return) do
|
|
23
|
-
results =
|
|
24
|
-
|
|
36
|
+
results = []
|
|
37
|
+
i = 0
|
|
38
|
+
n = collection.size
|
|
39
|
+
while i < n
|
|
40
|
+
item = collection[i]
|
|
41
|
+
scope_stack << item
|
|
25
42
|
begin
|
|
26
|
-
|
|
43
|
+
results << on_each(item, filter, scope_stack)
|
|
27
44
|
ensure
|
|
28
45
|
scope_stack.pop
|
|
29
46
|
end
|
|
47
|
+
i += 1
|
|
30
48
|
end
|
|
31
49
|
on_after(results, scope_stack)
|
|
32
50
|
end
|
|
33
51
|
early
|
|
34
52
|
end
|
|
35
53
|
|
|
36
|
-
def self.setup_collection(rules, scope_stack)
|
|
37
|
-
return handle_nil unless rules.is_a?(Array)
|
|
38
|
-
|
|
39
|
-
filter = rules[1]
|
|
40
|
-
return handle_nil if filter.nil? && raise_on_nil_filter?
|
|
41
|
-
|
|
42
|
-
collection_rule = rules.any? ? rules[0] : rules
|
|
43
|
-
return handle_nil if collection_rule.nil?
|
|
44
|
-
|
|
45
|
-
collection = Utils::Array.wrap(Engine.call(collection_rule, scope_stack))
|
|
46
|
-
[collection, filter]
|
|
47
|
-
end
|
|
48
|
-
|
|
49
54
|
def self.on_each(_item, filter, scope_stack)
|
|
50
55
|
Engine.call(filter, scope_stack)
|
|
51
56
|
end
|
|
@@ -53,10 +58,6 @@ module ShinyJsonLogic
|
|
|
53
58
|
def self.on_after(results, _scope_stack)
|
|
54
59
|
results
|
|
55
60
|
end
|
|
56
|
-
|
|
57
|
-
def self.handle_nil
|
|
58
|
-
raise Errors::InvalidArguments
|
|
59
|
-
end
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -7,7 +7,7 @@ module ShinyJsonLogic
|
|
|
7
7
|
module Operations
|
|
8
8
|
class Max < Base
|
|
9
9
|
def self.execute(rules, scope_stack)
|
|
10
|
-
Numericals::MinMaxCollection.
|
|
10
|
+
Numericals::MinMaxCollection.resolve(rules, scope_stack, :max)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -6,9 +6,20 @@ module ShinyJsonLogic
|
|
|
6
6
|
module Operations
|
|
7
7
|
class Merge < Base
|
|
8
8
|
def self.execute(rules, scope_stack)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
result = []
|
|
10
|
+
operands = Utils::Array.wrap_nil(rules)
|
|
11
|
+
i = 0
|
|
12
|
+
n = operands.size
|
|
13
|
+
while i < n
|
|
14
|
+
evaluated = evaluate(operands[i], scope_stack)
|
|
15
|
+
if evaluated.is_a?(Array)
|
|
16
|
+
result.concat(evaluated)
|
|
17
|
+
else
|
|
18
|
+
result << evaluated
|
|
19
|
+
end
|
|
20
|
+
i += 1
|
|
21
|
+
end
|
|
22
|
+
result
|
|
12
23
|
end
|
|
13
24
|
end
|
|
14
25
|
end
|
|
@@ -7,7 +7,7 @@ module ShinyJsonLogic
|
|
|
7
7
|
module Operations
|
|
8
8
|
class Min < Base
|
|
9
9
|
def self.execute(rules, scope_stack)
|
|
10
|
-
Numericals::MinMaxCollection.
|
|
10
|
+
Numericals::MinMaxCollection.resolve(rules, scope_stack, :min)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
end
|
|
@@ -7,34 +7,55 @@ module ShinyJsonLogic
|
|
|
7
7
|
module Operations
|
|
8
8
|
class Missing < Base
|
|
9
9
|
def self.execute(rules, scope_stack)
|
|
10
|
-
|
|
10
|
+
wrapped = Utils::Array.wrap_nil(rules)
|
|
11
11
|
keys = []
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
i = 0
|
|
13
|
+
n = wrapped.size
|
|
14
|
+
while i < n
|
|
15
|
+
evaluated = evaluate(wrapped[i], scope_stack)
|
|
16
|
+
if evaluated.is_a?(Array)
|
|
17
|
+
j = 0
|
|
18
|
+
m = evaluated.size
|
|
19
|
+
while j < m
|
|
20
|
+
keys << evaluated[j].to_s
|
|
21
|
+
j += 1
|
|
22
|
+
end
|
|
23
|
+
else
|
|
24
|
+
keys << evaluated.to_s
|
|
25
|
+
end
|
|
26
|
+
i += 1
|
|
15
27
|
end
|
|
16
|
-
|
|
28
|
+
|
|
29
|
+
current_data = scope_stack.last
|
|
17
30
|
return keys unless current_data.is_a?(Hash)
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
existing = {}
|
|
33
|
+
deep_keys(current_data, nil, existing)
|
|
34
|
+
|
|
35
|
+
result = []
|
|
36
|
+
i = 0
|
|
37
|
+
n = keys.size
|
|
38
|
+
while i < n
|
|
39
|
+
result << keys[i] unless existing.key?(keys[i])
|
|
40
|
+
i += 1
|
|
41
|
+
end
|
|
42
|
+
result
|
|
20
43
|
end
|
|
21
44
|
|
|
22
|
-
def self.deep_keys(hash, prefix
|
|
45
|
+
def self.deep_keys(hash, prefix, acc)
|
|
23
46
|
return unless hash.is_a?(Hash)
|
|
24
47
|
|
|
25
|
-
result = []
|
|
26
48
|
hash.each do |key, val|
|
|
27
49
|
key_s = key.to_s
|
|
28
50
|
full_key = prefix ? "#{prefix}.#{key_s}" : key_s
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
result.concat(nested)
|
|
51
|
+
if val.is_a?(Hash)
|
|
52
|
+
deep_keys(val, full_key, acc)
|
|
32
53
|
else
|
|
33
|
-
|
|
54
|
+
acc[full_key] = true
|
|
34
55
|
end
|
|
35
56
|
end
|
|
36
|
-
result
|
|
37
57
|
end
|
|
58
|
+
|
|
38
59
|
private_class_method :deep_keys
|
|
39
60
|
end
|
|
40
61
|
end
|
|
@@ -8,13 +8,29 @@ module ShinyJsonLogic
|
|
|
8
8
|
class MissingSome < Missing
|
|
9
9
|
def self.execute(rules, scope_stack)
|
|
10
10
|
min_required = evaluate(rules[0], scope_stack)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
raw_keys = evaluate(rules[1], scope_stack)
|
|
12
|
+
raw_keys_arr = wrap_nil(raw_keys)
|
|
13
|
+
keys = Array.new(raw_keys_arr.size)
|
|
14
|
+
i = 0
|
|
15
|
+
n = raw_keys_arr.size
|
|
16
|
+
while i < n
|
|
17
|
+
keys[i] = raw_keys_arr[i].to_s
|
|
18
|
+
i += 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
current_data = scope_stack.last
|
|
13
22
|
return keys unless current_data.is_a?(Hash) && rules.is_a?(Array)
|
|
14
23
|
|
|
15
|
-
data_keys = current_data.keys
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
data_keys = current_data.keys
|
|
25
|
+
data_keys_s = Array.new(data_keys.size)
|
|
26
|
+
j = 0
|
|
27
|
+
m = data_keys.size
|
|
28
|
+
while j < m
|
|
29
|
+
data_keys_s[j] = data_keys[j].to_s
|
|
30
|
+
j += 1
|
|
31
|
+
end
|
|
32
|
+
present = keys & data_keys_s
|
|
33
|
+
present.size >= min_required ? [] : keys - present
|
|
18
34
|
end
|
|
19
35
|
end
|
|
20
36
|
end
|
|
@@ -16,12 +16,15 @@ module ShinyJsonLogic
|
|
|
16
16
|
safe_arithmetic do
|
|
17
17
|
result = nil
|
|
18
18
|
count = 0
|
|
19
|
+
i = 0
|
|
20
|
+
n = operands.size
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
evaluated = evaluate(
|
|
22
|
+
while i < n
|
|
23
|
+
evaluated = evaluate(operands[i], scope_stack)
|
|
22
24
|
num = Numericals::Numerify.numerify(evaluated)
|
|
23
25
|
count += 1
|
|
24
26
|
result = result.nil? ? num : result.remainder(num)
|
|
27
|
+
i += 1
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
return handle_invalid_args if count < 2
|
|
@@ -15,9 +15,12 @@ module ShinyJsonLogic
|
|
|
15
15
|
return false if rules.empty?
|
|
16
16
|
|
|
17
17
|
result = nil
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
i = 0
|
|
19
|
+
n = rules.size
|
|
20
|
+
while i < n
|
|
21
|
+
result = evaluate(rules[i], scope_stack)
|
|
20
22
|
return result if Truthy.call(result)
|
|
23
|
+
i += 1
|
|
21
24
|
end
|
|
22
25
|
result
|
|
23
26
|
end
|
|
@@ -9,11 +9,13 @@ module ShinyJsonLogic
|
|
|
9
9
|
def self.call(rules, scope_stack)
|
|
10
10
|
# Preserve doesn't create new scopes - evaluates each item directly
|
|
11
11
|
collection = Utils::Array.wrap(rules)
|
|
12
|
-
|
|
13
|
-
results =
|
|
14
|
-
|
|
12
|
+
n = collection.size
|
|
13
|
+
results = Array.new(n)
|
|
14
|
+
i = 0
|
|
15
|
+
while i < n
|
|
16
|
+
results[i] = Engine.call(collection[i], scope_stack)
|
|
17
|
+
i += 1
|
|
15
18
|
end
|
|
16
|
-
|
|
17
19
|
results.size == 1 ? results.first : results
|
|
18
20
|
end
|
|
19
21
|
end
|
|
@@ -16,14 +16,17 @@ module ShinyJsonLogic
|
|
|
16
16
|
safe_arithmetic do
|
|
17
17
|
result = nil
|
|
18
18
|
count = 0
|
|
19
|
+
i = 0
|
|
20
|
+
n = operands.size
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
evaluated = evaluate(
|
|
22
|
+
while i < n
|
|
23
|
+
evaluated = evaluate(operands[i], scope_stack)
|
|
22
24
|
num = Numericals::Numerify.numerify(evaluated)
|
|
23
25
|
num = 0 if num.nil?
|
|
24
26
|
return handle_nan if num.nil?
|
|
25
27
|
count += 1
|
|
26
28
|
result = result.nil? ? num.to_f : result * num.to_f
|
|
29
|
+
i += 1
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
return 1 if count == 0
|
|
@@ -12,24 +12,26 @@ module ShinyJsonLogic
|
|
|
12
12
|
|
|
13
13
|
def self.call(rules, scope_stack)
|
|
14
14
|
rules = resolve_rules(rules, scope_stack)
|
|
15
|
+
filter = setup_filter(rules)
|
|
16
|
+
collection = setup_collection(rules, scope_stack)
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
accumulator = Engine.call(rules[2], scope_stack)
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
reduce_scope = { "current" => nil, "accumulator" => nil }
|
|
21
|
+
scope_stack << reduce_scope
|
|
20
22
|
|
|
21
|
-
reduce_scope = { "current" => nil, "accumulator" => nil }
|
|
22
|
-
|
|
23
|
-
collection.each do |item|
|
|
24
|
-
reduce_scope["current"] = item
|
|
25
|
-
reduce_scope["accumulator"] = accumulator
|
|
26
|
-
scope_stack.push(reduce_scope)
|
|
27
23
|
begin
|
|
28
|
-
|
|
24
|
+
i = 0
|
|
25
|
+
n = collection.size
|
|
26
|
+
while i < n
|
|
27
|
+
reduce_scope["current"] = collection[i]
|
|
28
|
+
reduce_scope["accumulator"] = accumulator
|
|
29
|
+
accumulator = Engine.call(filter, scope_stack)
|
|
30
|
+
i += 1
|
|
31
|
+
end
|
|
29
32
|
ensure
|
|
30
33
|
scope_stack.pop
|
|
31
34
|
end
|
|
32
|
-
end
|
|
33
35
|
|
|
34
36
|
safe_arithmetic { accumulator }
|
|
35
37
|
end
|
|
@@ -16,13 +16,16 @@ module ShinyJsonLogic
|
|
|
16
16
|
safe_arithmetic do
|
|
17
17
|
result = nil
|
|
18
18
|
count = 0
|
|
19
|
+
i = 0
|
|
20
|
+
n = operands.size
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
evaluated = evaluate(
|
|
22
|
+
while i < n
|
|
23
|
+
evaluated = evaluate(operands[i], scope_stack)
|
|
22
24
|
num = Numericals::Numerify.numerify(evaluated)
|
|
23
25
|
num = 0 if num.nil?
|
|
24
26
|
count += 1
|
|
25
27
|
result = result.nil? ? num : result - num
|
|
28
|
+
i += 1
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
return handle_invalid_args if count == 0
|
|
@@ -16,7 +16,7 @@ module ShinyJsonLogic
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
extracted_type = error_type.is_a?(Hash) && error_type.key?("type") ? error_type["type"] : error_type
|
|
19
|
-
extracted_type = scope_stack.
|
|
19
|
+
extracted_type = scope_stack.last["type"] if extracted_type.nil?
|
|
20
20
|
|
|
21
21
|
raise Errors::Base.new(type: extracted_type)
|
|
22
22
|
end
|
|
@@ -6,12 +6,15 @@ module ShinyJsonLogic
|
|
|
6
6
|
def self.call(rules, scope_stack)
|
|
7
7
|
items = Utils::Array.wrap_nil(rules)
|
|
8
8
|
last_error = nil
|
|
9
|
+
i = 0
|
|
10
|
+
n = items.size
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
while i < n
|
|
13
|
+
item = items[i]
|
|
11
14
|
# If previous item was an error, switch context to error payload
|
|
12
15
|
if last_error
|
|
13
|
-
scope_stack
|
|
14
|
-
scope_stack
|
|
16
|
+
scope_stack << {} # intermediate level for [[1]] access
|
|
17
|
+
scope_stack << last_error.payload
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
begin
|
|
@@ -34,6 +37,7 @@ module ShinyJsonLogic
|
|
|
34
37
|
|
|
35
38
|
last_error = e
|
|
36
39
|
end
|
|
40
|
+
i += 1
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
# All items were errors, re-raise the last one
|
|
@@ -8,11 +8,21 @@ module ShinyJsonLogic
|
|
|
8
8
|
module Operations
|
|
9
9
|
class Val < Base
|
|
10
10
|
def self.execute(rules, scope_stack)
|
|
11
|
+
# Fast path: null or empty → return current scope
|
|
12
|
+
if rules.nil?
|
|
13
|
+
return Utils::DataHash.wrap(scope_stack.last)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Fast path: single string key (most common case)
|
|
17
|
+
if rules.is_a?(String)
|
|
18
|
+
return Utils::DataHash.wrap(Utils::HashFetch.fetch(scope_stack.last, rules))
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
raw_keys = Utils::Array.wrap_nil(rules)
|
|
12
22
|
|
|
13
|
-
# {"val": []}
|
|
23
|
+
# {"val": []} - return current scope
|
|
14
24
|
if raw_keys.empty? || raw_keys == [nil]
|
|
15
|
-
return Utils::DataHash.wrap(scope_stack.
|
|
25
|
+
return Utils::DataHash.wrap(scope_stack.last)
|
|
16
26
|
end
|
|
17
27
|
|
|
18
28
|
# Check if first element is an array (scope navigation syntax)
|
|
@@ -29,12 +39,18 @@ module ShinyJsonLogic
|
|
|
29
39
|
end
|
|
30
40
|
|
|
31
41
|
levels = level_indicator.abs
|
|
32
|
-
return Utils::DataHash.wrap(
|
|
42
|
+
return Utils::DataHash.wrap(ScopeStack.resolve(scope_stack, levels, evaluated_keys))
|
|
33
43
|
end
|
|
34
44
|
|
|
35
|
-
# Normal case: {"val":
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
# Normal case: {"val": ["key1", "key2"]}
|
|
46
|
+
keys_n = raw_keys.size
|
|
47
|
+
keys = Array.new(keys_n)
|
|
48
|
+
ki = 0
|
|
49
|
+
while ki < keys_n
|
|
50
|
+
keys[ki] = evaluate(raw_keys[ki], scope_stack)
|
|
51
|
+
ki += 1
|
|
52
|
+
end
|
|
53
|
+
current_data = scope_stack.last
|
|
38
54
|
Utils::DataHash.wrap(dig_value(current_data, keys))
|
|
39
55
|
end
|
|
40
56
|
|
|
@@ -42,10 +58,15 @@ module ShinyJsonLogic
|
|
|
42
58
|
return nil if data.nil?
|
|
43
59
|
return data if keys.empty?
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
obj = data
|
|
62
|
+
i = 0
|
|
63
|
+
n = keys.size
|
|
64
|
+
while i < n
|
|
46
65
|
return nil if obj.nil?
|
|
47
|
-
Utils::HashFetch.fetch(obj,
|
|
66
|
+
obj = Utils::HashFetch.fetch(obj, keys[i].to_s)
|
|
67
|
+
i += 1
|
|
48
68
|
end
|
|
69
|
+
obj
|
|
49
70
|
end
|
|
50
71
|
private_class_method :dig_value
|
|
51
72
|
end
|
|
@@ -11,29 +11,35 @@ module ShinyJsonLogic
|
|
|
11
11
|
def self.execute(rules, scope_stack)
|
|
12
12
|
# Fast path: simple string key, no default
|
|
13
13
|
if rules.is_a?(String)
|
|
14
|
-
current_data = scope_stack.
|
|
14
|
+
current_data = scope_stack.last
|
|
15
15
|
if rules.empty?
|
|
16
|
-
return
|
|
16
|
+
return wrap(current_data)
|
|
17
17
|
end
|
|
18
|
-
return
|
|
18
|
+
return wrap(fetch_value(current_data, rules))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
items = Utils::Array.wrap_nil(rules)
|
|
22
22
|
key = evaluate(items[0], scope_stack)
|
|
23
23
|
default = items.length > 1 ? evaluate(items[1], scope_stack) : nil
|
|
24
|
-
current_data = scope_stack.
|
|
24
|
+
current_data = scope_stack.last
|
|
25
25
|
|
|
26
26
|
if key.nil? || key == ""
|
|
27
|
-
return
|
|
27
|
+
return wrap(current_data)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
result = fetch_value(current_data, key)
|
|
31
31
|
result = result.nil? ? default : result
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
default || scope_stack.
|
|
32
|
+
wrap(result)
|
|
33
|
+
rescue
|
|
34
|
+
default || scope_stack.last
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Only wrap Hash values — non-Hash values can't be confused with operators.
|
|
38
|
+
def self.wrap(value)
|
|
39
|
+
value.is_a?(::Hash) ? Utils::DataHash.wrap(value) : value
|
|
40
|
+
end
|
|
41
|
+
private_class_method :wrap
|
|
42
|
+
|
|
37
43
|
def self.fetch_value(obj, key)
|
|
38
44
|
return nil if obj.nil?
|
|
39
45
|
|
|
@@ -56,7 +56,7 @@ module ShinyJsonLogic
|
|
|
56
56
|
def self.operation?(value)
|
|
57
57
|
# Rules always have exactly 1 key — use each_key with early return
|
|
58
58
|
# instead of keys.any? which allocates an Array of keys first
|
|
59
|
-
value.each_key { |key| return SOLVER_KEYS.include?(key.
|
|
59
|
+
value.each_key { |key| return SOLVER_KEYS.include?(key.to_s) }
|
|
60
60
|
false
|
|
61
61
|
end
|
|
62
62
|
end
|
|
@@ -3,68 +3,43 @@
|
|
|
3
3
|
require "shiny_json_logic/utils/hash_fetch"
|
|
4
4
|
|
|
5
5
|
module ShinyJsonLogic
|
|
6
|
-
#
|
|
6
|
+
# Helpers for navigating the scope stack in nested iterators.
|
|
7
7
|
#
|
|
8
|
-
# The scope stack
|
|
9
|
-
#
|
|
10
|
-
# - `resolve(n, *keys)` - go up n levels, then access keys via dig
|
|
11
|
-
# - `push(scope)` / `pop` - manage stack during iteration
|
|
8
|
+
# The scope stack is a plain Array passed as an argument — no object instantiation.
|
|
9
|
+
# Each entry is a scope (Hash or value). The last entry is the current scope.
|
|
12
10
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
11
|
+
# Entering/exiting a scope:
|
|
12
|
+
# scope_stack << item (push)
|
|
13
|
+
# scope_stack.pop (pop)
|
|
14
|
+
# scope_stack.last (current)
|
|
17
15
|
#
|
|
18
|
-
#
|
|
19
|
-
# so no indifferent access is needed here.
|
|
16
|
+
# Cross-level navigation (val + [[n]] syntax) uses ScopeStack.resolve.
|
|
20
17
|
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@data_stack = [root_data]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Push a new scope onto the stack (when entering an iteration)
|
|
27
|
-
def push(data)
|
|
28
|
-
@data_stack << data
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Pop the top scope (when exiting an iteration)
|
|
32
|
-
def pop
|
|
33
|
-
@data_stack.pop if @data_stack.size > 1
|
|
34
|
-
end
|
|
18
|
+
module ScopeStack
|
|
19
|
+
module_function
|
|
35
20
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# Resolve a value by going up n levels and then accessing keys
|
|
42
|
-
#
|
|
43
|
-
# @param levels [Integer] number of levels to go up (0 = current, 1 = parent, etc.)
|
|
44
|
-
# @param keys [Array] keys to dig into after reaching the target scope
|
|
45
|
-
# @return [Object] the resolved value
|
|
46
|
-
def resolve(levels, *keys)
|
|
47
|
-
target_index = @data_stack.size - 1 - levels
|
|
21
|
+
# Resolve a value by going up n levels and then accessing keys.
|
|
22
|
+
# Used by val.rb for {"val": [[n], "key"]} syntax.
|
|
23
|
+
def resolve(stack, levels, keys)
|
|
24
|
+
target_index = stack.size - 1 - levels
|
|
48
25
|
return nil if target_index < 0
|
|
49
26
|
|
|
50
|
-
data =
|
|
51
|
-
|
|
52
|
-
if keys.empty?
|
|
53
|
-
data
|
|
54
|
-
else
|
|
55
|
-
dig_value(data, keys)
|
|
56
|
-
end
|
|
27
|
+
data = stack[target_index]
|
|
28
|
+
keys.empty? ? data : dig_value(data, keys)
|
|
57
29
|
end
|
|
58
30
|
|
|
59
|
-
private
|
|
60
|
-
|
|
61
31
|
def dig_value(data, keys)
|
|
62
32
|
return nil if data.nil?
|
|
63
33
|
|
|
64
|
-
|
|
34
|
+
obj = data
|
|
35
|
+
i = 0
|
|
36
|
+
n = keys.size
|
|
37
|
+
while i < n
|
|
65
38
|
return nil if obj.nil?
|
|
66
|
-
Utils::HashFetch.fetch(obj,
|
|
39
|
+
obj = Utils::HashFetch.fetch(obj, keys[i].to_s)
|
|
40
|
+
i += 1
|
|
67
41
|
end
|
|
42
|
+
obj
|
|
68
43
|
end
|
|
69
44
|
end
|
|
70
45
|
end
|
|
@@ -9,9 +9,17 @@ module ShinyJsonLogic
|
|
|
9
9
|
when true, false then subject
|
|
10
10
|
when Numeric then !subject.zero?
|
|
11
11
|
when String, Hash then !subject.empty?
|
|
12
|
-
when Array then subject.any?
|
|
13
12
|
when NilClass then false
|
|
14
|
-
|
|
13
|
+
when Array
|
|
14
|
+
i = 0
|
|
15
|
+
n = subject.size
|
|
16
|
+
while i < n
|
|
17
|
+
return true if subject[i]
|
|
18
|
+
i += 1
|
|
19
|
+
end
|
|
20
|
+
false
|
|
21
|
+
else
|
|
22
|
+
true
|
|
15
23
|
end
|
|
16
24
|
end
|
|
17
25
|
end
|
|
@@ -8,15 +8,12 @@ module ShinyJsonLogic
|
|
|
8
8
|
def wrap(object)
|
|
9
9
|
return [] if object.nil?
|
|
10
10
|
return object if object.is_a?(::Array)
|
|
11
|
-
return object.to_ary || [object] if object.respond_to?(:to_ary)
|
|
12
11
|
|
|
13
12
|
[object]
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def wrap_nil(object)
|
|
17
|
-
return [nil] if object.nil?
|
|
18
16
|
return object if object.is_a?(::Array)
|
|
19
|
-
return object.to_ary || [object] if object.respond_to?(:to_ary)
|
|
20
17
|
|
|
21
18
|
[object]
|
|
22
19
|
end
|
data/lib/shiny_json_logic.rb
CHANGED