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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +2 -2
  5. data/lib/shiny_json_logic/engine.rb +12 -5
  6. data/lib/shiny_json_logic/numericals/min_max_collection.rb +21 -13
  7. data/lib/shiny_json_logic/operations/addition.rb +6 -2
  8. data/lib/shiny_json_logic/operations/and.rb +5 -2
  9. data/lib/shiny_json_logic/operations/base.rb +8 -4
  10. data/lib/shiny_json_logic/operations/coalesce.rb +5 -2
  11. data/lib/shiny_json_logic/operations/concatenation.rb +18 -5
  12. data/lib/shiny_json_logic/operations/division.rb +5 -2
  13. data/lib/shiny_json_logic/operations/exists.rb +8 -8
  14. data/lib/shiny_json_logic/operations/filter.rb +11 -5
  15. data/lib/shiny_json_logic/operations/inclusion.rb +8 -1
  16. data/lib/shiny_json_logic/operations/iterable/base.rb +23 -22
  17. data/lib/shiny_json_logic/operations/max.rb +1 -1
  18. data/lib/shiny_json_logic/operations/merge.rb +14 -3
  19. data/lib/shiny_json_logic/operations/min.rb +1 -1
  20. data/lib/shiny_json_logic/operations/missing.rb +34 -13
  21. data/lib/shiny_json_logic/operations/missing_some.rb +21 -5
  22. data/lib/shiny_json_logic/operations/modulo.rb +5 -2
  23. data/lib/shiny_json_logic/operations/or.rb +5 -2
  24. data/lib/shiny_json_logic/operations/preserve.rb +6 -4
  25. data/lib/shiny_json_logic/operations/product.rb +5 -2
  26. data/lib/shiny_json_logic/operations/reduce.rb +13 -11
  27. data/lib/shiny_json_logic/operations/subtraction.rb +5 -2
  28. data/lib/shiny_json_logic/operations/throw.rb +1 -1
  29. data/lib/shiny_json_logic/operations/try.rb +7 -3
  30. data/lib/shiny_json_logic/operations/val.rb +29 -8
  31. data/lib/shiny_json_logic/operations/var.rb +14 -8
  32. data/lib/shiny_json_logic/operator_solver.rb +1 -1
  33. data/lib/shiny_json_logic/scope_stack.rb +23 -48
  34. data/lib/shiny_json_logic/truthy.rb +10 -2
  35. data/lib/shiny_json_logic/utils/array.rb +0 -3
  36. data/lib/shiny_json_logic/version.rb +1 -1
  37. data/lib/shiny_json_logic.rb +1 -1
  38. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ebe927021766f9f1fd019339cbe471dd9d8102e6639cb2c075eb5bf2cf2618c
4
- data.tar.gz: a419046895f9b3cdafe511f8a14d39ba641076dec9b41d9c7ada7b6dafc56a8e
3
+ metadata.gz: cb0d5385d70e0de02275f1a5a4ad0f8d6014c9f8030df64882cb7da1ff44942d
4
+ data.tar.gz: a608c50e2ef5acb46a348674df1ffeff928274f142f5d71ffbaf1101787b4149
5
5
  SHA512:
6
- metadata.gz: efbb1ee13c8fb44f15df4d5ea1d1d6d347fb67e6552059afb54c21b754b1442a4f4b2b4d66b19fbcf804bc28861dbd44e90693797b7badf1d5ecd9adea1a096e
7
- data.tar.gz: 0036f2a1623dc4f2fbcd16298aa799412402a2437c7d04aefa3e30b1f0b9f3010b5bf88899f6b98d2a2078416fda7d9047cf683f25a0624d4ecab9cc7df742a8
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiny_json_logic (0.3.5)
4
+ shiny_json_logic (0.3.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Compatibility](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/luismoyano/shiny-json-logic-ruby/master/badges/compat.json)
4
4
  [![Gem Version](https://badge.fury.io/rb/shiny_json_logic.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/shiny_json_logic)
5
- ![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.6-brightgreen)
5
+ ![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.4-brightgreen)
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.6+ compatible**, one of the lowest minimum versions supported in the Ruby ecosystem.
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?(Utils::DataHash)
12
- rule
13
- elsif rule.is_a?(Hash)
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
- rule.map { |val| call(val, scope_stack) }
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 collect_numeric_values(rules, scope_stack)
11
- values = collect_values(rules, scope_stack)
12
- raise Errors::InvalidArguments if values.empty?
13
- values.each { |v| raise Errors::InvalidArguments unless v.is_a?(Numeric) }
14
- values
15
- end
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
- wrapped = Utils::Array.wrap_nil(rules)
24
- wrapped.map { |rule| Engine.call(rule, scope_stack) }
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
- Utils::Array.wrap_nil(rules).each do |rule|
16
- val = Numericals::Numerify.numerify(evaluate(rule, scope_stack))
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
- rules.each do |rule|
19
- result = evaluate(rule, scope_stack)
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
- dynamic = op?(rules)
17
- raise Errors::InvalidArguments if dynamic && raise_on_dynamic_args?
18
-
19
- return Engine.call(rules, scope_stack) if dynamic
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
- rules.each do |rule|
10
- result = evaluate(rule, scope_stack)
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).each do |rule|
11
- evaluated = evaluate(rule, scope_stack)
12
- Utils::Array.wrap_nil(evaluated).each { |v| result << v.to_s }
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.join
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
- operands.each do |rule|
21
- evaluated = evaluate(rule, scope_stack)
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.current
10
-
11
- Utils::Array.wrap_nil(rules).each do |rule|
12
- segment = evaluate(rule, scope_stack)
13
- return false unless current.key?(segment)
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
- collection, filter = setup_collection(rules, scope_stack)
16
-
17
- collection.each_with_object([]) do |item, acc|
18
- scope_stack.push(item)
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
- acc << item if Truthy.call(Engine.call(filter, scope_stack))
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
- haystack.any? { |el| (el.is_a?(Symbol) ? el.to_s : el) == needle }
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, filter = setup_collection(rules, scope_stack)
32
+ filter = setup_filter(rules)
33
+ collection = setup_collection(rules, scope_stack)
21
34
 
22
35
  early = catch(:early_return) do
23
- results = collection.each_with_object([]) do |item, acc|
24
- scope_stack.push(item)
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
- acc << on_each(item, filter, scope_stack)
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.collect_numeric_values(rules, scope_stack).max
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
- Utils::Array.wrap_nil(rules).map do |rule|
10
- Utils::Array.wrap_nil(evaluate(rule, scope_stack))
11
- end.reduce([], :+)
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.collect_numeric_values(rules, scope_stack).min
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
- items = Utils::Array.wrap_nil(rules)
10
+ wrapped = Utils::Array.wrap_nil(rules)
11
11
  keys = []
12
- items.each do |rule|
13
- evaluated = evaluate(rule, scope_stack)
14
- keys.concat(Utils::Array.wrap_nil(evaluated).map(&:to_s))
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
- current_data = scope_stack.current
28
+
29
+ current_data = scope_stack.last
17
30
  return keys unless current_data.is_a?(Hash)
18
31
 
19
- keys - deep_keys(current_data)
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 = nil)
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
- nested = deep_keys(val, full_key)
30
- if nested
31
- result.concat(nested)
51
+ if val.is_a?(Hash)
52
+ deep_keys(val, full_key, acc)
32
53
  else
33
- result << full_key
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
- keys = Utils::Array.wrap_nil(evaluate(rules[1], scope_stack)).map(&:to_s)
12
- current_data = scope_stack.current
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.map(&:to_s)
16
- present = keys & data_keys
17
- present.size >= min_required ? [] : Missing.execute(keys, scope_stack)
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
- operands.each do |rule|
21
- evaluated = evaluate(rule, scope_stack)
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
- rules.each do |rule|
19
- result = evaluate(rule, scope_stack)
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 = collection.each_with_object([]) do |item, acc|
14
- acc << Engine.call(item, scope_stack)
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
- operands.each do |rule|
21
- evaluated = evaluate(rule, scope_stack)
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
- collection, filter = setup_collection(rules, scope_stack)
18
+ accumulator = Engine.call(rules[2], scope_stack)
17
19
 
18
- # Evaluate initial accumulator (third argument)
19
- accumulator = Engine.call(rules[2], scope_stack)
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
- accumulator = Engine.call(filter, scope_stack)
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
- operands.each do |rule|
21
- evaluated = evaluate(rule, scope_stack)
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.current["type"] if extracted_type.nil?
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
- items.each do |item|
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.push({}) # intermediate level for [[1]] access
14
- scope_stack.push(last_error.payload)
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": []} or {"val": null} - return current scope
23
+ # {"val": []} - return current scope
14
24
  if raw_keys.empty? || raw_keys == [nil]
15
- return Utils::DataHash.wrap(scope_stack.current)
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(scope_stack.resolve(levels, *evaluated_keys))
42
+ return Utils::DataHash.wrap(ScopeStack.resolve(scope_stack, levels, evaluated_keys))
33
43
  end
34
44
 
35
- # Normal case: {"val": "key"} or {"val": ["key1", "key2"]}
36
- keys = raw_keys.map { |rule| evaluate(rule, scope_stack) }
37
- current_data = scope_stack.current
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
- keys.reduce(data) do |obj, key|
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, key.to_s)
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.current
14
+ current_data = scope_stack.last
15
15
  if rules.empty?
16
- return Utils::DataHash.wrap(current_data)
16
+ return wrap(current_data)
17
17
  end
18
- return Utils::DataHash.wrap(fetch_value(current_data, rules))
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.current
24
+ current_data = scope_stack.last
25
25
 
26
26
  if key.nil? || key == ""
27
- return Utils::DataHash.wrap(current_data)
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
- Utils::DataHash.wrap(result)
33
- rescue
34
- default || scope_stack.current
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.is_a?(String) ? key : key.to_s) }
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
- # Manages a stack of scopes for nested data access in iterators.
6
+ # Helpers for navigating the scope stack in nested iterators.
7
7
  #
8
- # The scope stack allows:
9
- # - `current` - returns the top of the stack (current item in iterator)
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
- # When inside an iterator like map:
14
- # {"val": []} -> returns current scope (the item being iterated)
15
- # {"val": [[1], "key"]} -> go up 1 level, access "key"
16
- # {"val": [[2], "key"]} -> go up 2 levels, access "key"
11
+ # Entering/exiting a scope:
12
+ # scope_stack << item (push)
13
+ # scope_stack.pop (pop)
14
+ # scope_stack.last (current)
17
15
  #
18
- # Note: Data is normalized to string keys upfront in ShinyJsonLogic.apply,
19
- # so no indifferent access is needed here.
16
+ # Cross-level navigation (val + [[n]] syntax) uses ScopeStack.resolve.
20
17
  #
21
- class ScopeStack
22
- def initialize(root_data)
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
- # Returns the current scope's data (top of stack)
37
- def current
38
- @data_stack.last
39
- end
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 = @data_stack[target_index]
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
- keys.reduce(data) do |obj, key|
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, key.to_s)
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
- else true
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ShinyJsonLogic
4
- VERSION = "0.3.5"
4
+ VERSION = "0.3.6"
5
5
  end
@@ -11,7 +11,7 @@ require "shiny_json_logic/scope_stack"
11
11
 
12
12
  module ShinyJsonLogic
13
13
  def self.apply(rule, data = {})
14
- scope_stack = ScopeStack.new(data || {})
14
+ scope_stack = [data || {}]
15
15
  Engine.call(rule, scope_stack)
16
16
  end
17
17
  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.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luis Moyano