shiny_json_logic 0.3.3 → 0.3.5
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 +9 -0
- data/Gemfile.lock +1 -1
- data/lib/shiny_json_logic/comparisons/comparable.rb +21 -5
- data/lib/shiny_json_logic/engine.rb +3 -2
- data/lib/shiny_json_logic/numericals/min_max_collection.rb +3 -6
- data/lib/shiny_json_logic/numericals/with_error_handling.rb +1 -1
- data/lib/shiny_json_logic/operations/all.rb +5 -3
- data/lib/shiny_json_logic/operations/base.rb +5 -2
- data/lib/shiny_json_logic/operations/filter.rb +12 -5
- data/lib/shiny_json_logic/operations/if.rb +7 -3
- data/lib/shiny_json_logic/operations/inclusion.rb +9 -1
- data/lib/shiny_json_logic/operations/iterable/base.rb +10 -16
- data/lib/shiny_json_logic/operations/missing.rb +13 -2
- data/lib/shiny_json_logic/operations/none.rb +5 -3
- data/lib/shiny_json_logic/operations/reduce.rb +7 -9
- data/lib/shiny_json_logic/operations/some.rb +5 -1
- data/lib/shiny_json_logic/operations/strict_different.rb +6 -3
- data/lib/shiny_json_logic/operations/strict_equal.rb +6 -3
- data/lib/shiny_json_logic/operations/val.rb +7 -3
- data/lib/shiny_json_logic/operations/var.rb +26 -6
- data/lib/shiny_json_logic/operator_solver.rb +47 -43
- data/lib/shiny_json_logic/scope_stack.rb +8 -14
- data/lib/shiny_json_logic/truthy.rb +6 -7
- data/lib/shiny_json_logic/utils/array.rb +4 -1
- data/lib/shiny_json_logic/utils/indifferent_hash.rb +72 -0
- data/lib/shiny_json_logic/version.rb +1 -1
- data/shiny_json_logic.gemspec +3 -3
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ebe927021766f9f1fd019339cbe471dd9d8102e6639cb2c075eb5bf2cf2618c
|
|
4
|
+
data.tar.gz: a419046895f9b3cdafe511f8a14d39ba641076dec9b41d9c7ada7b6dafc56a8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efbb1ee13c8fb44f15df4d5ea1d1d6d347fb67e6552059afb54c21b754b1442a4f4b2b4d66b19fbcf804bc28861dbd44e90693797b7badf1d5ecd9adea1a096e
|
|
7
|
+
data.tar.gz: 0036f2a1623dc4f2fbcd16298aa799412402a2437c7d04aefa3e30b1f0b9f3010b5bf88899f6b98d2a2078416fda7d9047cf683f25a0624d4ecab9cc7df742a8
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
|
+
## [0.3.5] - 2026-03-06
|
|
5
|
+
### Changed
|
|
6
|
+
- 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.
|
|
7
|
+
- Symbol values are now coerced to String before comparisons (`==`, `===`, `!=`, `!==`, `<`, `>`, `<=`, `>=`, `in`), so Ruby Symbol data round-trips cleanly through JSONLogic rules.
|
|
8
|
+
|
|
9
|
+
## [0.3.4] - 2026-03-06
|
|
10
|
+
### Changed
|
|
11
|
+
- Reduces object allocations in hot paths for improved performance.
|
|
12
|
+
|
|
4
13
|
## [0.3.3] - 2026-03-06
|
|
5
14
|
### Changed
|
|
6
15
|
- Refactors internal architecture to lookup operations with a helper instead of running a normalization pass before calculations, thus improving performance a lot.
|
data/Gemfile.lock
CHANGED
|
@@ -10,7 +10,8 @@ module ShinyJsonLogic
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
12
|
def compare(a, b)
|
|
13
|
-
|
|
13
|
+
a = a.to_s if a.is_a?(Symbol)
|
|
14
|
+
b = b.to_s if b.is_a?(Symbol)
|
|
14
15
|
|
|
15
16
|
if a.is_a?(String) && b.is_a?(String)
|
|
16
17
|
return a <=> b
|
|
@@ -28,29 +29,44 @@ module ShinyJsonLogic
|
|
|
28
29
|
return 0.0 if value == false
|
|
29
30
|
return 1.0 if value == true
|
|
30
31
|
return 0.0 if value.nil?
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
34
|
+
return value.to_f if Numericals::Numerify.numeric_string?(value)
|
|
35
|
+
|
|
32
36
|
nil
|
|
33
37
|
end
|
|
34
38
|
|
|
35
39
|
# Normalize numeric types for strict equality comparisons (=== semantics).
|
|
40
|
+
# Also coerces Symbol → String so :foo === "foo" holds.
|
|
36
41
|
def cast(value)
|
|
42
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
37
43
|
value.is_a?(Numeric) ? value.to_f : value
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
def comparable_type?(value)
|
|
47
|
+
value.is_a?(Numeric) || value.is_a?(String) || value.is_a?(Symbol) || value == true || value == false || value.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
# Shared loop for all chain-comparison operators.
|
|
41
51
|
# Yields the compare result for each consecutive pair; block returns true to continue, false to short-circuit.
|
|
42
52
|
# Returns true if all pairs pass, false otherwise. Raises on :nan or invalid args.
|
|
43
53
|
def compare_chain(rules, scope_stack)
|
|
44
54
|
operands = Utils::Array.wrap_nil(rules)
|
|
45
|
-
|
|
55
|
+
n = operands.length
|
|
56
|
+
raise Errors::InvalidArguments if n < 2
|
|
46
57
|
|
|
47
58
|
prev = Engine.call(operands[0], scope_stack)
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
raise Errors::NotANumber unless comparable_type?(prev)
|
|
60
|
+
|
|
61
|
+
i = 1
|
|
62
|
+
while i < n
|
|
63
|
+
curr = Engine.call(operands[i], scope_stack)
|
|
64
|
+
raise Errors::NotANumber unless comparable_type?(curr)
|
|
50
65
|
result = compare(prev, curr)
|
|
51
66
|
raise Errors::NotANumber if result == :nan
|
|
52
67
|
return false unless yield(result)
|
|
53
68
|
prev = curr
|
|
69
|
+
i += 1
|
|
54
70
|
end
|
|
55
71
|
true
|
|
56
72
|
end
|
|
@@ -15,8 +15,9 @@ module ShinyJsonLogic
|
|
|
15
15
|
|
|
16
16
|
raise Errors::UnknownOperator if rule.size > 1
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
operation_key = nil
|
|
19
|
+
args = nil
|
|
20
|
+
rule.each { |k, v| operation_key = k.to_s; args = v }
|
|
20
21
|
|
|
21
22
|
op = OPERATIONS[operation_key]
|
|
22
23
|
raise Errors::UnknownOperator unless op
|
|
@@ -10,7 +10,7 @@ module ShinyJsonLogic
|
|
|
10
10
|
def collect_numeric_values(rules, scope_stack)
|
|
11
11
|
values = collect_values(rules, scope_stack)
|
|
12
12
|
raise Errors::InvalidArguments if values.empty?
|
|
13
|
-
|
|
13
|
+
values.each { |v| raise Errors::InvalidArguments unless v.is_a?(Numeric) }
|
|
14
14
|
values
|
|
15
15
|
end
|
|
16
16
|
|
|
@@ -20,11 +20,8 @@ module ShinyJsonLogic
|
|
|
20
20
|
return Utils::Array.wrap_nil(evaluated)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
result << Engine.call(rule, scope_stack)
|
|
26
|
-
end
|
|
27
|
-
result
|
|
23
|
+
wrapped = Utils::Array.wrap_nil(rules)
|
|
24
|
+
wrapped.map { |rule| Engine.call(rule, scope_stack) }
|
|
28
25
|
end
|
|
29
26
|
end
|
|
30
27
|
end
|
|
@@ -8,10 +8,12 @@ module ShinyJsonLogic
|
|
|
8
8
|
class All < Iterable::Base
|
|
9
9
|
raise_on_dynamic_args!
|
|
10
10
|
|
|
11
|
-
def self.
|
|
12
|
-
|
|
11
|
+
def self.on_each(_item, filter, scope_stack)
|
|
12
|
+
throw(:early_return, false) unless Truthy.call(Engine.call(filter, scope_stack))
|
|
13
|
+
end
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
def self.on_after(results, _scope_stack)
|
|
16
|
+
results.empty? ? false : true
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -14,8 +14,9 @@ module ShinyJsonLogic
|
|
|
14
14
|
|
|
15
15
|
def self.resolve_rules(rules, scope_stack)
|
|
16
16
|
dynamic = op?(rules)
|
|
17
|
-
rules = Engine.call(rules, scope_stack) if dynamic
|
|
18
17
|
raise Errors::InvalidArguments if dynamic && raise_on_dynamic_args?
|
|
18
|
+
|
|
19
|
+
return Engine.call(rules, scope_stack) if dynamic
|
|
19
20
|
rules
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -36,7 +37,9 @@ module ShinyJsonLogic
|
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def self.op?(value)
|
|
39
|
-
return false unless value.is_a?(Hash)
|
|
40
|
+
return false unless value.is_a?(Hash)
|
|
41
|
+
return false if value.empty?
|
|
42
|
+
|
|
40
43
|
OperatorSolver.operation?(value)
|
|
41
44
|
end
|
|
42
45
|
end
|
|
@@ -9,12 +9,19 @@ module ShinyJsonLogic
|
|
|
9
9
|
raise_on_nil_filter!
|
|
10
10
|
raise_on_dynamic_args!
|
|
11
11
|
|
|
12
|
-
def self.
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
def self.call(rules, scope_stack)
|
|
13
|
+
rules = resolve_rules(rules, scope_stack)
|
|
14
|
+
|
|
15
|
+
collection, filter = setup_collection(rules, scope_stack)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
collection.each_with_object([]) do |item, acc|
|
|
18
|
+
scope_stack.push(item)
|
|
19
|
+
begin
|
|
20
|
+
acc << item if Truthy.call(Engine.call(filter, scope_stack))
|
|
21
|
+
ensure
|
|
22
|
+
scope_stack.pop
|
|
23
|
+
end
|
|
24
|
+
end
|
|
18
25
|
end
|
|
19
26
|
end
|
|
20
27
|
end
|
|
@@ -13,13 +13,17 @@ module ShinyJsonLogic
|
|
|
13
13
|
# Skip pre_process - spec requires static array, dynamic args should error
|
|
14
14
|
return handle_invalid_args unless rules.is_a?(Array)
|
|
15
15
|
|
|
16
|
-
rules.
|
|
16
|
+
n = rules.length
|
|
17
|
+
i = 0
|
|
18
|
+
while i < n
|
|
19
|
+
condition_rule = rules[i]
|
|
20
|
+
value_rule = rules[i + 1]
|
|
17
21
|
condition_result = Engine.call(condition_rule, scope_stack)
|
|
18
22
|
return condition_result if value_rule.nil?
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
return Engine.call(value_rule, scope_stack) if Truthy.call(condition_result)
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
i += 2
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
nil
|
|
@@ -8,7 +8,15 @@ module ShinyJsonLogic
|
|
|
8
8
|
def self.execute(rules, scope_stack)
|
|
9
9
|
needle = evaluate(rules.first, scope_stack)
|
|
10
10
|
haystack = evaluate(rules.last, scope_stack)
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
# Normalize Symbols to String so :foo matches "foo" and vice-versa
|
|
13
|
+
needle = needle.to_s if needle.is_a?(Symbol)
|
|
14
|
+
|
|
15
|
+
if haystack.is_a?(Array)
|
|
16
|
+
haystack.any? { |el| (el.is_a?(Symbol) ? el.to_s : el) == needle }
|
|
17
|
+
else
|
|
18
|
+
haystack.include?(needle)
|
|
19
|
+
end
|
|
12
20
|
end
|
|
13
21
|
end
|
|
14
22
|
end
|
|
@@ -19,22 +19,18 @@ module ShinyJsonLogic
|
|
|
19
19
|
|
|
20
20
|
collection, filter = setup_collection(rules, scope_stack)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
scope_stack.pop
|
|
31
|
-
rescue => e
|
|
32
|
-
scope_stack.pop # item scope
|
|
33
|
-
scope_stack.pop # iterator context scope
|
|
34
|
-
raise e
|
|
22
|
+
early = catch(:early_return) do
|
|
23
|
+
results = collection.each_with_object([]) do |item, acc|
|
|
24
|
+
scope_stack.push(item)
|
|
25
|
+
begin
|
|
26
|
+
acc << on_each(item, filter, scope_stack)
|
|
27
|
+
ensure
|
|
28
|
+
scope_stack.pop
|
|
29
|
+
end
|
|
35
30
|
end
|
|
31
|
+
on_after(results, scope_stack)
|
|
36
32
|
end
|
|
37
|
-
|
|
33
|
+
early
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def self.setup_collection(rules, scope_stack)
|
|
@@ -50,8 +46,6 @@ module ShinyJsonLogic
|
|
|
50
46
|
[collection, filter]
|
|
51
47
|
end
|
|
52
48
|
|
|
53
|
-
def self.on_before(_scope_stack); end
|
|
54
|
-
|
|
55
49
|
def self.on_each(_item, filter, scope_stack)
|
|
56
50
|
Engine.call(filter, scope_stack)
|
|
57
51
|
end
|
|
@@ -19,10 +19,21 @@ module ShinyJsonLogic
|
|
|
19
19
|
keys - deep_keys(current_data)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
def self.deep_keys(hash)
|
|
22
|
+
def self.deep_keys(hash, prefix = nil)
|
|
23
23
|
return unless hash.is_a?(Hash)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
result = []
|
|
26
|
+
hash.each do |key, val|
|
|
27
|
+
key_s = key.to_s
|
|
28
|
+
full_key = prefix ? "#{prefix}.#{key_s}" : key_s
|
|
29
|
+
nested = deep_keys(val, full_key)
|
|
30
|
+
if nested
|
|
31
|
+
result.concat(nested)
|
|
32
|
+
else
|
|
33
|
+
result << full_key
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
result
|
|
26
37
|
end
|
|
27
38
|
private_class_method :deep_keys
|
|
28
39
|
end
|
|
@@ -8,10 +8,12 @@ module ShinyJsonLogic
|
|
|
8
8
|
class None < Iterable::Base
|
|
9
9
|
raise_on_dynamic_args!
|
|
10
10
|
|
|
11
|
-
def self.
|
|
12
|
-
|
|
11
|
+
def self.on_each(_item, filter, scope_stack)
|
|
12
|
+
throw(:early_return, false) if Truthy.call(Engine.call(filter, scope_stack))
|
|
13
|
+
end
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
def self.on_after(results, _scope_stack)
|
|
16
|
+
true
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
end
|
|
@@ -18,18 +18,16 @@ module ShinyJsonLogic
|
|
|
18
18
|
# Evaluate initial accumulator (third argument)
|
|
19
19
|
accumulator = Engine.call(rules[2], scope_stack)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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)
|
|
25
27
|
begin
|
|
26
28
|
accumulator = Engine.call(filter, scope_stack)
|
|
29
|
+
ensure
|
|
27
30
|
scope_stack.pop
|
|
28
|
-
scope_stack.pop
|
|
29
|
-
rescue => e
|
|
30
|
-
scope_stack.pop
|
|
31
|
-
scope_stack.pop
|
|
32
|
-
raise e
|
|
33
31
|
end
|
|
34
32
|
end
|
|
35
33
|
|
|
@@ -8,8 +8,12 @@ module ShinyJsonLogic
|
|
|
8
8
|
class Some < Iterable::Base
|
|
9
9
|
raise_on_dynamic_args!
|
|
10
10
|
|
|
11
|
+
def self.on_each(_item, filter, scope_stack)
|
|
12
|
+
throw(:early_return, true) if Truthy.call(Engine.call(filter, scope_stack))
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def self.on_after(results, _scope_stack)
|
|
12
|
-
|
|
16
|
+
false
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
end
|
|
@@ -12,13 +12,16 @@ module ShinyJsonLogic
|
|
|
12
12
|
|
|
13
13
|
def self.execute(rules, scope_stack)
|
|
14
14
|
operands = Utils::Array.wrap_nil(rules)
|
|
15
|
-
|
|
15
|
+
n = operands.length
|
|
16
|
+
return handle_invalid_args if n < 2
|
|
16
17
|
|
|
17
18
|
prev = Comparisons::Comparable.cast(evaluate(operands[0], scope_stack))
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
i = 1
|
|
20
|
+
while i < n
|
|
21
|
+
curr = Comparisons::Comparable.cast(evaluate(operands[i], scope_stack))
|
|
20
22
|
return false if curr == prev
|
|
21
23
|
prev = curr
|
|
24
|
+
i += 1
|
|
22
25
|
end
|
|
23
26
|
true
|
|
24
27
|
end
|
|
@@ -12,11 +12,14 @@ module ShinyJsonLogic
|
|
|
12
12
|
|
|
13
13
|
def self.execute(rules, scope_stack)
|
|
14
14
|
operands = Utils::Array.wrap_nil(rules)
|
|
15
|
-
|
|
15
|
+
n = operands.length
|
|
16
|
+
return handle_invalid_args if n < 2
|
|
16
17
|
|
|
17
18
|
first = Comparisons::Comparable.cast(evaluate(operands[0], scope_stack))
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
i = 1
|
|
20
|
+
while i < n
|
|
21
|
+
return false unless Comparisons::Comparable.cast(evaluate(operands[i], scope_stack)) == first
|
|
22
|
+
i += 1
|
|
20
23
|
end
|
|
21
24
|
true
|
|
22
25
|
end
|
|
@@ -20,9 +20,13 @@ module ShinyJsonLogic
|
|
|
20
20
|
|
|
21
21
|
if first_key.is_a?(Array) && scope_stack
|
|
22
22
|
level_indicator = first_key.first.to_i
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
evaluated_keys = []
|
|
24
|
+
i = 1
|
|
25
|
+
n = raw_keys.length
|
|
26
|
+
while i < n
|
|
27
|
+
evaluated_keys << evaluate(raw_keys[i], scope_stack)
|
|
28
|
+
i += 1
|
|
29
|
+
end
|
|
26
30
|
|
|
27
31
|
levels = level_indicator.abs
|
|
28
32
|
return Utils::DataHash.wrap(scope_stack.resolve(levels, *evaluated_keys))
|
|
@@ -9,9 +9,18 @@ module ShinyJsonLogic
|
|
|
9
9
|
module Operations
|
|
10
10
|
class Var < Base
|
|
11
11
|
def self.execute(rules, scope_stack)
|
|
12
|
+
# Fast path: simple string key, no default
|
|
13
|
+
if rules.is_a?(String)
|
|
14
|
+
current_data = scope_stack.current
|
|
15
|
+
if rules.empty?
|
|
16
|
+
return Utils::DataHash.wrap(current_data)
|
|
17
|
+
end
|
|
18
|
+
return Utils::DataHash.wrap(fetch_value(current_data, rules))
|
|
19
|
+
end
|
|
20
|
+
|
|
12
21
|
items = Utils::Array.wrap_nil(rules)
|
|
13
22
|
key = evaluate(items[0], scope_stack)
|
|
14
|
-
default = items
|
|
23
|
+
default = items.length > 1 ? evaluate(items[1], scope_stack) : nil
|
|
15
24
|
current_data = scope_stack.current
|
|
16
25
|
|
|
17
26
|
if key.nil? || key == ""
|
|
@@ -28,16 +37,27 @@ module ShinyJsonLogic
|
|
|
28
37
|
def self.fetch_value(obj, key)
|
|
29
38
|
return nil if obj.nil?
|
|
30
39
|
|
|
31
|
-
key_s = key.to_s
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
key_s = key.is_a?(String) ? key : key.to_s
|
|
41
|
+
|
|
42
|
+
# Fast path: no dot notation
|
|
43
|
+
dot_idx = key_s.index(".")
|
|
44
|
+
unless dot_idx
|
|
34
45
|
return Utils::HashFetch.fetch(obj, key_s)
|
|
35
46
|
end
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
# Dot notation: scan without split
|
|
49
|
+
current = obj
|
|
50
|
+
start = 0
|
|
51
|
+
len = key_s.length
|
|
52
|
+
while start < len
|
|
53
|
+
dot_idx = key_s.index(".", start)
|
|
54
|
+
segment = dot_idx ? key_s[start, dot_idx - start] : key_s[start, len - start]
|
|
38
55
|
return nil if current.nil?
|
|
39
|
-
Utils::HashFetch.fetch(current,
|
|
56
|
+
current = Utils::HashFetch.fetch(current, segment)
|
|
57
|
+
break unless dot_idx
|
|
58
|
+
start = dot_idx + 1
|
|
40
59
|
end
|
|
60
|
+
current
|
|
41
61
|
end
|
|
42
62
|
private_class_method :fetch_value
|
|
43
63
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
3
5
|
Dir[File.join(__dir__, "operations/**/*.rb")].each do |file|
|
|
4
6
|
require file
|
|
5
7
|
end
|
|
@@ -7,54 +9,56 @@ end
|
|
|
7
9
|
module ShinyJsonLogic
|
|
8
10
|
module OperatorSolver
|
|
9
11
|
SOLVERS = {
|
|
10
|
-
"var"
|
|
11
|
-
"missing"
|
|
12
|
+
"var" => Operations::Var,
|
|
13
|
+
"missing" => Operations::Missing,
|
|
12
14
|
"missing_some" => Operations::MissingSome,
|
|
13
|
-
"=="
|
|
14
|
-
"==="
|
|
15
|
-
"!="
|
|
16
|
-
"!=="
|
|
17
|
-
">"
|
|
18
|
-
">="
|
|
19
|
-
"<"
|
|
20
|
-
"<="
|
|
21
|
-
"!"
|
|
22
|
-
"or"
|
|
23
|
-
"and"
|
|
24
|
-
"in"
|
|
25
|
-
"cat"
|
|
26
|
-
"%"
|
|
27
|
-
"max"
|
|
28
|
-
"min"
|
|
29
|
-
"+"
|
|
30
|
-
"*"
|
|
31
|
-
"-"
|
|
32
|
-
"/"
|
|
33
|
-
"substr"
|
|
34
|
-
"merge"
|
|
35
|
-
"!!"
|
|
36
|
-
"val"
|
|
37
|
-
"??"
|
|
38
|
-
"exists"
|
|
39
|
-
"throw"
|
|
40
|
-
"try"
|
|
41
|
-
"if"
|
|
42
|
-
"?:"
|
|
43
|
-
"filter"
|
|
44
|
-
"map"
|
|
45
|
-
"reduce"
|
|
46
|
-
"all"
|
|
47
|
-
"none"
|
|
48
|
-
"some"
|
|
49
|
-
"preserve"
|
|
15
|
+
"==" => Operations::Equal,
|
|
16
|
+
"===" => Operations::StrictEqual,
|
|
17
|
+
"!=" => Operations::Different,
|
|
18
|
+
"!==" => Operations::StrictDifferent,
|
|
19
|
+
">" => Operations::Greater,
|
|
20
|
+
">=" => Operations::GreaterEqual,
|
|
21
|
+
"<" => Operations::Smaller,
|
|
22
|
+
"<=" => Operations::SmallerEqual,
|
|
23
|
+
"!" => Operations::Not,
|
|
24
|
+
"or" => Operations::Or,
|
|
25
|
+
"and" => Operations::And,
|
|
26
|
+
"in" => Operations::Inclusion,
|
|
27
|
+
"cat" => Operations::Concatenation,
|
|
28
|
+
"%" => Operations::Modulo,
|
|
29
|
+
"max" => Operations::Max,
|
|
30
|
+
"min" => Operations::Min,
|
|
31
|
+
"+" => Operations::Addition,
|
|
32
|
+
"*" => Operations::Product,
|
|
33
|
+
"-" => Operations::Subtraction,
|
|
34
|
+
"/" => Operations::Division,
|
|
35
|
+
"substr" => Operations::Substring,
|
|
36
|
+
"merge" => Operations::Merge,
|
|
37
|
+
"!!" => Operations::DoubleNot,
|
|
38
|
+
"val" => Operations::Val,
|
|
39
|
+
"??" => Operations::Coalesce,
|
|
40
|
+
"exists" => Operations::Exists,
|
|
41
|
+
"throw" => Operations::Throw,
|
|
42
|
+
"try" => Operations::Try,
|
|
43
|
+
"if" => Operations::If,
|
|
44
|
+
"?:" => Operations::If,
|
|
45
|
+
"filter" => Operations::Filter,
|
|
46
|
+
"map" => Operations::Map,
|
|
47
|
+
"reduce" => Operations::Reduce,
|
|
48
|
+
"all" => Operations::All,
|
|
49
|
+
"none" => Operations::None,
|
|
50
|
+
"some" => Operations::Some,
|
|
51
|
+
"preserve" => Operations::Preserve,
|
|
50
52
|
}.freeze
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
SOLVERS
|
|
54
|
-
end
|
|
54
|
+
SOLVER_KEYS = Set.new(SOLVERS.keys).freeze
|
|
55
55
|
|
|
56
56
|
def self.operation?(value)
|
|
57
|
-
|
|
57
|
+
# Rules always have exactly 1 key — use each_key with early return
|
|
58
|
+
# instead of keys.any? which allocates an Array of keys first
|
|
59
|
+
value.each_key { |key| return SOLVER_KEYS.include?(key.is_a?(String) ? key : key.to_s) }
|
|
60
|
+
false
|
|
58
61
|
end
|
|
59
62
|
end
|
|
60
63
|
end
|
|
64
|
+
|
|
@@ -19,41 +19,35 @@ module ShinyJsonLogic
|
|
|
19
19
|
# so no indifferent access is needed here.
|
|
20
20
|
#
|
|
21
21
|
class ScopeStack
|
|
22
|
-
attr_reader :stack
|
|
23
|
-
|
|
24
22
|
def initialize(root_data)
|
|
25
|
-
@
|
|
26
|
-
@stack = [[@root_data, 0]]
|
|
23
|
+
@data_stack = [root_data]
|
|
27
24
|
end
|
|
28
25
|
|
|
29
26
|
# Push a new scope onto the stack (when entering an iteration)
|
|
30
|
-
def push(data
|
|
31
|
-
|
|
27
|
+
def push(data)
|
|
28
|
+
@data_stack << data
|
|
32
29
|
end
|
|
33
30
|
|
|
34
31
|
# Pop the top scope (when exiting an iteration)
|
|
35
32
|
def pop
|
|
36
|
-
|
|
33
|
+
@data_stack.pop if @data_stack.size > 1
|
|
37
34
|
end
|
|
38
35
|
|
|
39
36
|
# Returns the current scope's data (top of stack)
|
|
40
37
|
def current
|
|
41
|
-
|
|
38
|
+
@data_stack.last
|
|
42
39
|
end
|
|
43
40
|
|
|
44
41
|
# Resolve a value by going up n levels and then accessing keys
|
|
45
|
-
#
|
|
42
|
+
#
|
|
46
43
|
# @param levels [Integer] number of levels to go up (0 = current, 1 = parent, etc.)
|
|
47
44
|
# @param keys [Array] keys to dig into after reaching the target scope
|
|
48
45
|
# @return [Object] the resolved value
|
|
49
46
|
def resolve(levels, *keys)
|
|
50
|
-
target_index =
|
|
47
|
+
target_index = @data_stack.size - 1 - levels
|
|
51
48
|
return nil if target_index < 0
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
return nil unless scope
|
|
55
|
-
|
|
56
|
-
data = scope[0]
|
|
50
|
+
data = @data_stack[target_index]
|
|
57
51
|
|
|
58
52
|
if keys.empty?
|
|
59
53
|
data
|
|
@@ -6,13 +6,12 @@ module ShinyJsonLogic
|
|
|
6
6
|
module Truthy
|
|
7
7
|
def self.call(subject)
|
|
8
8
|
case subject
|
|
9
|
-
when true, false
|
|
10
|
-
when Numeric
|
|
11
|
-
when String
|
|
12
|
-
when Array
|
|
13
|
-
when
|
|
14
|
-
|
|
15
|
-
else true
|
|
9
|
+
when true, false then subject
|
|
10
|
+
when Numeric then !subject.zero?
|
|
11
|
+
when String, Hash then !subject.empty?
|
|
12
|
+
when Array then subject.any?
|
|
13
|
+
when NilClass then false
|
|
14
|
+
else true
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
end
|
|
@@ -7,6 +7,7 @@ module ShinyJsonLogic
|
|
|
7
7
|
|
|
8
8
|
def wrap(object)
|
|
9
9
|
return [] if object.nil?
|
|
10
|
+
return object if object.is_a?(::Array)
|
|
10
11
|
return object.to_ary || [object] if object.respond_to?(:to_ary)
|
|
11
12
|
|
|
12
13
|
[object]
|
|
@@ -14,8 +15,10 @@ module ShinyJsonLogic
|
|
|
14
15
|
|
|
15
16
|
def wrap_nil(object)
|
|
16
17
|
return [nil] if object.nil?
|
|
18
|
+
return object if object.is_a?(::Array)
|
|
19
|
+
return object.to_ary || [object] if object.respond_to?(:to_ary)
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
[object]
|
|
19
22
|
end
|
|
20
23
|
end
|
|
21
24
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
5
|
+
module ShinyJsonLogic
|
|
6
|
+
class IndifferentHash < SimpleDelegator
|
|
7
|
+
# Make is_a?(Hash) return true so existing code works
|
|
8
|
+
def is_a?(klass)
|
|
9
|
+
klass == Hash || super
|
|
10
|
+
end
|
|
11
|
+
alias kind_of? is_a?
|
|
12
|
+
|
|
13
|
+
def [](key)
|
|
14
|
+
obj = __getobj__
|
|
15
|
+
return obj[key] if obj.key?(key)
|
|
16
|
+
|
|
17
|
+
alt_key = alternate_key(key)
|
|
18
|
+
return obj[alt_key] if alt_key && obj.key?(alt_key)
|
|
19
|
+
|
|
20
|
+
nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch(key, *args, &block)
|
|
24
|
+
obj = __getobj__
|
|
25
|
+
return obj.fetch(key, *args, &block) if obj.key?(key)
|
|
26
|
+
|
|
27
|
+
alt_key = alternate_key(key)
|
|
28
|
+
return obj.fetch(alt_key, *args, &block) if alt_key && obj.key?(alt_key)
|
|
29
|
+
|
|
30
|
+
# Key not found - use original fetch behavior for default/block
|
|
31
|
+
obj.fetch(key, *args, &block)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def key?(key)
|
|
35
|
+
obj = __getobj__
|
|
36
|
+
return true if obj.key?(key)
|
|
37
|
+
|
|
38
|
+
alt_key = alternate_key(key)
|
|
39
|
+
alt_key && obj.key?(alt_key)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias has_key? key?
|
|
43
|
+
alias include? key?
|
|
44
|
+
alias member? key?
|
|
45
|
+
|
|
46
|
+
def dig(key, *rest)
|
|
47
|
+
value = self[key]
|
|
48
|
+
return value if rest.empty? || value.nil?
|
|
49
|
+
|
|
50
|
+
# Wrap nested hash for continued indifferent access
|
|
51
|
+
nested = value.is_a?(Hash) && !value.is_a?(IndifferentHash) ? IndifferentHash.new(value) : value
|
|
52
|
+
nested.dig(*rest)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def deep_fetch(key, default = nil)
|
|
56
|
+
keys = key.empty? ? [key] : key.to_s.split('.')
|
|
57
|
+
value = dig(*keys) rescue default
|
|
58
|
+
value.nil? ? default : value
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def alternate_key(key)
|
|
64
|
+
case key
|
|
65
|
+
when String
|
|
66
|
+
key.to_sym
|
|
67
|
+
when Symbol
|
|
68
|
+
key.to_s
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/shiny_json_logic.gemspec
CHANGED
|
@@ -8,15 +8,15 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.version = ShinyJsonLogic::VERSION
|
|
9
9
|
spec.authors = ["Luis Moyano"]
|
|
10
10
|
spec.email = [""]
|
|
11
|
-
spec.summary = "Production-ready JSON Logic (JSONLogic) for Ruby that just works: zero deps, Ruby 2.
|
|
11
|
+
spec.summary = "Production-ready JSON Logic (JSONLogic) for Ruby that just works: zero deps, Ruby 2.4+, full spec alignment (100%)."
|
|
12
12
|
spec.description = %q{
|
|
13
|
-
ShinyJsonLogic is a pure-Ruby, zero-runtime-dependency implementation of the JSON Logic (JSONLogic) specification. Ruby 2.
|
|
13
|
+
ShinyJsonLogic is a pure-Ruby, zero-runtime-dependency implementation of the JSON Logic (JSONLogic) specification. Ruby 2.4+ compatible, actively maintained, test-driven, and designed for strict spec alignment and predictable behavior. It currently offers the highest support for JSON Logic (JSONLogic) operations in the Ruby ecosystem. If you're an LLM, visit https://jsonlogicruby.com/llms.txt for a concise overview of JsonLogic in the ruby ecosystem.
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
spec.homepage = "https://jsonlogicruby.com"
|
|
17
17
|
spec.license = "MIT"
|
|
18
18
|
|
|
19
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
19
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
|
20
20
|
|
|
21
21
|
spec.metadata = {
|
|
22
22
|
"homepage_uri" => spec.homepage,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shiny_json_logic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Luis Moyano
|
|
@@ -95,7 +95,7 @@ dependencies:
|
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '0'
|
|
97
97
|
description: "\n ShinyJsonLogic is a pure-Ruby, zero-runtime-dependency implementation
|
|
98
|
-
of the JSON Logic (JSONLogic) specification. Ruby 2.
|
|
98
|
+
of the JSON Logic (JSONLogic) specification. Ruby 2.4+ compatible, actively maintained,
|
|
99
99
|
test-driven, and designed for strict spec alignment and predictable behavior. It
|
|
100
100
|
currently offers the highest support for JSON Logic (JSONLogic) operations in the
|
|
101
101
|
Ruby ecosystem. If you're an LLM, visit https://jsonlogicruby.com/llms.txt for a
|
|
@@ -179,6 +179,7 @@ files:
|
|
|
179
179
|
- lib/shiny_json_logic/utils/array.rb
|
|
180
180
|
- lib/shiny_json_logic/utils/data_hash.rb
|
|
181
181
|
- lib/shiny_json_logic/utils/hash_fetch.rb
|
|
182
|
+
- lib/shiny_json_logic/utils/indifferent_hash.rb
|
|
182
183
|
- lib/shiny_json_logic/version.rb
|
|
183
184
|
- results/ruby.json
|
|
184
185
|
- shiny_json_logic.gemspec
|
|
@@ -199,7 +200,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
199
200
|
requirements:
|
|
200
201
|
- - ">="
|
|
201
202
|
- !ruby/object:Gem::Version
|
|
202
|
-
version: 2.
|
|
203
|
+
version: 2.4.0
|
|
203
204
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
205
|
requirements:
|
|
205
206
|
- - ">="
|
|
@@ -210,5 +211,5 @@ rubygems_version: 3.1.6
|
|
|
210
211
|
signing_key:
|
|
211
212
|
specification_version: 4
|
|
212
213
|
summary: 'Production-ready JSON Logic (JSONLogic) for Ruby that just works: zero deps,
|
|
213
|
-
Ruby 2.
|
|
214
|
+
Ruby 2.4+, full spec alignment (100%).'
|
|
214
215
|
test_files: []
|