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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 763e1776b60f327617c84ae32574996acc7029c1ade284482a839ec9e8590f34
4
- data.tar.gz: 7208db5ae0a84a4119d67b2a281651eb40b68daea9aedf9fcc73aa0fbb2ab8db
3
+ metadata.gz: 1ebe927021766f9f1fd019339cbe471dd9d8102e6639cb2c075eb5bf2cf2618c
4
+ data.tar.gz: a419046895f9b3cdafe511f8a14d39ba641076dec9b41d9c7ada7b6dafc56a8e
5
5
  SHA512:
6
- metadata.gz: 924234efc1cf60dc46ecbe8daa9a666ebeaeeef84d015a217d8706ea5860b2b4386755b38d5b97e212e2d3817acab1412daf699a3416fad2622092021df84645
7
- data.tar.gz: 70eccfb4595ca8af441d1ebe69a8b06b33f87c779d21619a0d1fcbc6e9ea10cf4d4ea85b3380e8ecdb744237b729a3e838ab470d6128219adbe5ced8c35490ad
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiny_json_logic (0.3.3)
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,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
- 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.
43
53
  def compare_chain(rules, scope_stack)
44
54
  operands = Utils::Array.wrap_nil(rules)
45
- raise Errors::InvalidArguments if operands.length < 2
55
+ n = operands.length
56
+ raise Errors::InvalidArguments if n < 2
46
57
 
47
58
  prev = Engine.call(operands[0], scope_stack)
48
- operands[1..].each do |rule|
49
- curr = Engine.call(rule, scope_stack)
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
- operation, args = rule.first
19
- operation_key = operation.to_s
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
- 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
 
@@ -20,11 +20,8 @@ module ShinyJsonLogic
20
20
  return Utils::Array.wrap_nil(evaluated)
21
21
  end
22
22
 
23
- result = []
24
- Utils::Array.wrap_nil(rules).each do |rule|
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
@@ -9,7 +9,7 @@ module ShinyJsonLogic
9
9
  module WithErrorHandling
10
10
  module_function
11
11
 
12
- def safe_arithmetic(&block)
12
+ def safe_arithmetic
13
13
  result = yield
14
14
  if result.to_f.nan? || result == Float::INFINITY || result == -Float::INFINITY
15
15
  return handle_nan
@@ -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
@@ -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.each_slice(2) do |condition_rule, value_rule|
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
- next unless Truthy.call(condition_result)
24
+ return Engine.call(value_rule, scope_stack) if Truthy.call(condition_result)
21
25
 
22
- return Engine.call(value_rule, scope_stack)
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
- 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,22 +19,18 @@ module ShinyJsonLogic
19
19
 
20
20
  collection, filter = setup_collection(rules, scope_stack)
21
21
 
22
- on_before(scope_stack)
23
- results = collection.each_with_index.each_with_object([]) do |(item, index), acc|
24
- scope_stack.push({ "index" => index }, index: index)
25
- scope_stack.push(item, index: index)
26
- begin
27
- solved = on_each(item, filter, scope_stack)
28
- acc << solved
29
- scope_stack.pop
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
- on_after(results, scope_stack)
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
- hash.keys.map { |key| ([key.to_s] << deep_keys(hash[key])).compact.join(".") }
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.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,18 +18,16 @@ module ShinyJsonLogic
18
18
  # Evaluate initial accumulator (third argument)
19
19
  accumulator = Engine.call(rules[2], scope_stack)
20
20
 
21
- collection.each_with_index do |item, index|
22
- scope_stack.push({ "index" => index }, index: index)
23
- reduce_scope = { "current" => item, "accumulator" => accumulator }
24
- scope_stack.push(reduce_scope, index: index)
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
- results.any? { |res| res == true }
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
- return handle_invalid_args if operands.length < 2
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
- operands[1..].each do |rule|
19
- curr = Comparisons::Comparable.cast(evaluate(rule, scope_stack))
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
- return handle_invalid_args if operands.length < 2
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
- operands[1..].each do |rule|
19
- return false unless Comparisons::Comparable.cast(evaluate(rule, scope_stack)) == first
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
- remaining_keys = raw_keys[1..]
24
-
25
- evaluated_keys = remaining_keys.map { |rule| evaluate(rule, scope_stack) }
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[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
@@ -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" => Operations::Var,
11
- "missing" => Operations::Missing,
12
+ "var" => Operations::Var,
13
+ "missing" => Operations::Missing,
12
14
  "missing_some" => Operations::MissingSome,
13
- "==" => Operations::Equal,
14
- "===" => Operations::StrictEqual,
15
- "!=" => Operations::Different,
16
- "!==" => Operations::StrictDifferent,
17
- ">" => Operations::Greater,
18
- ">=" => Operations::GreaterEqual,
19
- "<" => Operations::Smaller,
20
- "<=" => Operations::SmallerEqual,
21
- "!" => Operations::Not,
22
- "or" => Operations::Or,
23
- "and" => Operations::And,
24
- "in" => Operations::Inclusion,
25
- "cat" => Operations::Concatenation,
26
- "%" => Operations::Modulo,
27
- "max" => Operations::Max,
28
- "min" => Operations::Min,
29
- "+" => Operations::Addition,
30
- "*" => Operations::Product,
31
- "-" => Operations::Subtraction,
32
- "/" => Operations::Division,
33
- "substr" => Operations::Substring,
34
- "merge" => Operations::Merge,
35
- "!!" => Operations::DoubleNot,
36
- "val" => Operations::Val,
37
- "??" => Operations::Coalesce,
38
- "exists" => Operations::Exists,
39
- "throw" => Operations::Throw,
40
- "try" => Operations::Try,
41
- "if" => Operations::If,
42
- "?:" => Operations::If,
43
- "filter" => Operations::Filter,
44
- "map" => Operations::Map,
45
- "reduce" => Operations::Reduce,
46
- "all" => Operations::All,
47
- "none" => Operations::None,
48
- "some" => Operations::Some,
49
- "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,
50
52
  }.freeze
51
53
 
52
- def self.solvers
53
- SOLVERS
54
- end
54
+ SOLVER_KEYS = Set.new(SOLVERS.keys).freeze
55
55
 
56
56
  def self.operation?(value)
57
- value.keys.any? { |key| SOLVERS.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
+
@@ -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
- @root_data = root_data
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, index: 0)
31
- stack.push([data, index])
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
- stack.pop if stack.size > 1
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
- stack.last[0]
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 = stack.size - 1 - levels
47
+ target_index = @data_stack.size - 1 - levels
51
48
  return nil if target_index < 0
52
49
 
53
- scope = stack[target_index]
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 then subject
10
- when Numeric then !subject.zero?
11
- when String then !subject.empty?
12
- when Array then subject.any?
13
- when Hash then !subject.empty?
14
- when NilClass then false
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
- wrap(object)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShinyJsonLogic
4
- VERSION = "0.3.3"
4
+ VERSION = "0.3.5"
5
5
  end
@@ -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.6+, full spec alignment (100%)."
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.6+ 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.
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.6.0")
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.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.6+ compatible, actively maintained,
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.6.0
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.6+, full spec alignment (100%).'
214
+ Ruby 2.4+, full spec alignment (100%).'
214
215
  test_files: []