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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf0275f81efb49af13061d1c54c2dc273fb4686ad647a43f3ba4eafb069176f5
4
- data.tar.gz: d411aa52d5d3a235b5a9c783909acac800e10a620e0294f572ccbca9d7421223
3
+ metadata.gz: 1ebe927021766f9f1fd019339cbe471dd9d8102e6639cb2c075eb5bf2cf2618c
4
+ data.tar.gz: a419046895f9b3cdafe511f8a14d39ba641076dec9b41d9c7ada7b6dafc56a8e
5
5
  SHA512:
6
- metadata.gz: a926de52b731c63259ba2a6a954e35dd0d94c133d78cfb4bfe8c7d862cf8f5d5f9e6a33ad8b9d63028d9cc9cfa97473f6fca92683c27a219bc5838bcd3d626eb
7
- data.tar.gz: 112ec835a660537716dd1c94cfa9c152584296a44b857d0161ac3a2dd69c6f2440fd2ccf15542907f2b7a83aeb38a99acce949ad06fb55be62ff20d4687ec55a
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiny_json_logic (0.3.4)
4
+ shiny_json_logic (0.3.5)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -10,7 +10,8 @@ module ShinyJsonLogic
10
10
  module_function
11
11
 
12
12
  def compare(a, b)
13
- return :nan if a.is_a?(Array) || a.is_a?(Hash) || b.is_a?(Array) || b.is_a?(Hash)
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
- return value.to_f if value.is_a?(String) && Numericals::Numerify.numeric_string?(value)
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
- raise Errors::InvalidArguments unless values.all? { |v| v.is_a?(Numeric) }
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.on_after(results, _scope_stack)
12
- return false if results.empty?
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
- results.all? { |res| Truthy.call(res) }
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) && !value.empty?
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.on_each(item, filter, scope_stack)
13
- Truthy.call(Engine.call(filter, scope_stack)) ? item : nil
14
- end
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
- def self.on_after(results, _scope_stack)
17
- results.compact
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
- haystack.include?(needle)
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
- index_scope = { "index" => 0 }
23
- on_before(scope_stack)
24
- results = collection.each_with_index.each_with_object([]) do |(item, index), acc|
25
- index_scope["index"] = index
26
- scope_stack.push(index_scope, index: index)
27
- scope_stack.push(item, index: index)
28
- begin
29
- solved = on_each(item, filter, scope_stack)
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
- on_after(results, scope_stack)
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.on_after(results, _scope_stack)
12
- return true if results.empty?
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
- results.none? { |res| Truthy.call(res) }
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.each_with_index do |item, index|
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(index_scope, index: index)
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
- results.any? { |res| res == true }
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[1] ? evaluate(items[1], scope_stack) : nil
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
- # Fast path: no dot notation, single key lookup
33
- unless key_s.include?(".")
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
- key_s.split(".").reduce(obj) do |current, k|
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, k)
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" => Operations::Var,
13
- "missing" => Operations::Missing,
12
+ "var" => Operations::Var,
13
+ "missing" => Operations::Missing,
14
14
  "missing_some" => Operations::MissingSome,
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,
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
- value.keys.any? { |key| SOLVER_KEYS.include?(key.is_a?(String) ? key : key.to_s) }
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 = [root_data]
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, index: 0)
29
- @data_stack << data
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)
@@ -16,8 +16,9 @@ module ShinyJsonLogic
16
16
  def wrap_nil(object)
17
17
  return [nil] if object.nil?
18
18
  return object if object.is_a?(::Array)
19
+ return object.to_ary || [object] if object.respond_to?(:to_ary)
19
20
 
20
- wrap(object)
21
+ [object]
21
22
  end
22
23
  end
23
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShinyJsonLogic
4
- VERSION = "0.3.4"
4
+ VERSION = "0.3.5"
5
5
  end
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
4
+ version: 0.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luis Moyano