shiny_json_logic 0.3.4 → 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 +5 -0
- data/Gemfile.lock +1 -1
- data/lib/shiny_json_logic/comparisons/comparable.rb +15 -2
- data/lib/shiny_json_logic/numericals/min_max_collection.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/inclusion.rb +9 -1
- data/lib/shiny_json_logic/operations/iterable/base.rb +10 -18
- data/lib/shiny_json_logic/operations/none.rb +5 -3
- data/lib/shiny_json_logic/operations/reduce.rb +3 -10
- data/lib/shiny_json_logic/operations/some.rb +5 -1
- data/lib/shiny_json_logic/operations/var.rb +26 -6
- data/lib/shiny_json_logic/operator_solver.rb +44 -40
- data/lib/shiny_json_logic/scope_stack.rb +4 -9
- data/lib/shiny_json_logic/utils/array.rb +2 -1
- data/lib/shiny_json_logic/version.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: 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,11 @@
|
|
|
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
|
+
|
|
4
9
|
## [0.3.4] - 2026-03-06
|
|
5
10
|
### Changed
|
|
6
11
|
- Reduces object allocations in hot paths for improved performance.
|
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,15 +29,24 @@ 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.
|
|
@@ -46,9 +56,12 @@ module ShinyJsonLogic
|
|
|
46
56
|
raise Errors::InvalidArguments if n < 2
|
|
47
57
|
|
|
48
58
|
prev = Engine.call(operands[0], scope_stack)
|
|
59
|
+
raise Errors::NotANumber unless comparable_type?(prev)
|
|
60
|
+
|
|
49
61
|
i = 1
|
|
50
62
|
while i < n
|
|
51
63
|
curr = Engine.call(operands[i], scope_stack)
|
|
64
|
+
raise Errors::NotANumber unless comparable_type?(curr)
|
|
52
65
|
result = compare(prev, curr)
|
|
53
66
|
raise Errors::NotANumber if result == :nan
|
|
54
67
|
return false unless yield(result)
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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,24 +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
|
-
acc << solved
|
|
31
|
-
scope_stack.pop
|
|
32
|
-
scope_stack.pop
|
|
33
|
-
rescue => e
|
|
34
|
-
scope_stack.pop # item scope
|
|
35
|
-
scope_stack.pop # iterator context scope
|
|
36
|
-
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
|
|
37
30
|
end
|
|
31
|
+
on_after(results, scope_stack)
|
|
38
32
|
end
|
|
39
|
-
|
|
33
|
+
early
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
def self.setup_collection(rules, scope_stack)
|
|
@@ -52,8 +46,6 @@ module ShinyJsonLogic
|
|
|
52
46
|
[collection, filter]
|
|
53
47
|
end
|
|
54
48
|
|
|
55
|
-
def self.on_before(_scope_stack); end
|
|
56
|
-
|
|
57
49
|
def self.on_each(_item, filter, scope_stack)
|
|
58
50
|
Engine.call(filter, scope_stack)
|
|
59
51
|
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,23 +18,16 @@ module ShinyJsonLogic
|
|
|
18
18
|
# Evaluate initial accumulator (third argument)
|
|
19
19
|
accumulator = Engine.call(rules[2], scope_stack)
|
|
20
20
|
|
|
21
|
-
index_scope = { "index" => 0 }
|
|
22
21
|
reduce_scope = { "current" => nil, "accumulator" => nil }
|
|
23
22
|
|
|
24
|
-
collection.
|
|
25
|
-
index_scope["index"] = index
|
|
23
|
+
collection.each do |item|
|
|
26
24
|
reduce_scope["current"] = item
|
|
27
25
|
reduce_scope["accumulator"] = accumulator
|
|
28
|
-
scope_stack.push(
|
|
29
|
-
scope_stack.push(reduce_scope, index: index)
|
|
26
|
+
scope_stack.push(reduce_scope)
|
|
30
27
|
begin
|
|
31
28
|
accumulator = Engine.call(filter, scope_stack)
|
|
29
|
+
ensure
|
|
32
30
|
scope_stack.pop
|
|
33
|
-
scope_stack.pop
|
|
34
|
-
rescue => e
|
|
35
|
-
scope_stack.pop
|
|
36
|
-
scope_stack.pop
|
|
37
|
-
raise e
|
|
38
31
|
end
|
|
39
32
|
end
|
|
40
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
|
|
@@ -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
|
|
@@ -9,52 +9,56 @@ end
|
|
|
9
9
|
module ShinyJsonLogic
|
|
10
10
|
module OperatorSolver
|
|
11
11
|
SOLVERS = {
|
|
12
|
-
"var"
|
|
13
|
-
"missing"
|
|
12
|
+
"var" => Operations::Var,
|
|
13
|
+
"missing" => Operations::Missing,
|
|
14
14
|
"missing_some" => Operations::MissingSome,
|
|
15
|
-
"=="
|
|
16
|
-
"==="
|
|
17
|
-
"!="
|
|
18
|
-
"!=="
|
|
19
|
-
">"
|
|
20
|
-
">="
|
|
21
|
-
"<"
|
|
22
|
-
"<="
|
|
23
|
-
"!"
|
|
24
|
-
"or"
|
|
25
|
-
"and"
|
|
26
|
-
"in"
|
|
27
|
-
"cat"
|
|
28
|
-
"%"
|
|
29
|
-
"max"
|
|
30
|
-
"min"
|
|
31
|
-
"+"
|
|
32
|
-
"*"
|
|
33
|
-
"-"
|
|
34
|
-
"/"
|
|
35
|
-
"substr"
|
|
36
|
-
"merge"
|
|
37
|
-
"!!"
|
|
38
|
-
"val"
|
|
39
|
-
"??"
|
|
40
|
-
"exists"
|
|
41
|
-
"throw"
|
|
42
|
-
"try"
|
|
43
|
-
"if"
|
|
44
|
-
"?:"
|
|
45
|
-
"filter"
|
|
46
|
-
"map"
|
|
47
|
-
"reduce"
|
|
48
|
-
"all"
|
|
49
|
-
"none"
|
|
50
|
-
"some"
|
|
51
|
-
"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,
|
|
52
52
|
}.freeze
|
|
53
53
|
|
|
54
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
|
+
|
|
@@ -20,22 +20,17 @@ module ShinyJsonLogic
|
|
|
20
20
|
#
|
|
21
21
|
class ScopeStack
|
|
22
22
|
def initialize(root_data)
|
|
23
|
-
@data_stack
|
|
24
|
-
@index_stack = [0]
|
|
23
|
+
@data_stack = [root_data]
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
# Push a new scope onto the stack (when entering an iteration)
|
|
28
|
-
def push(data
|
|
29
|
-
@data_stack
|
|
30
|
-
@index_stack << index
|
|
27
|
+
def push(data)
|
|
28
|
+
@data_stack << data
|
|
31
29
|
end
|
|
32
30
|
|
|
33
31
|
# Pop the top scope (when exiting an iteration)
|
|
34
32
|
def pop
|
|
35
|
-
if @data_stack.size > 1
|
|
36
|
-
@data_stack.pop
|
|
37
|
-
@index_stack.pop
|
|
38
|
-
end
|
|
33
|
+
@data_stack.pop if @data_stack.size > 1
|
|
39
34
|
end
|
|
40
35
|
|
|
41
36
|
# Returns the current scope's data (top of stack)
|