shiny_json_logic 0.3.3 → 0.3.4

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: bf0275f81efb49af13061d1c54c2dc273fb4686ad647a43f3ba4eafb069176f5
4
+ data.tar.gz: d411aa52d5d3a235b5a9c783909acac800e10a620e0294f572ccbca9d7421223
5
5
  SHA512:
6
- metadata.gz: 924234efc1cf60dc46ecbe8daa9a666ebeaeeef84d015a217d8706ea5860b2b4386755b38d5b97e212e2d3817acab1412daf699a3416fad2622092021df84645
7
- data.tar.gz: 70eccfb4595ca8af441d1ebe69a8b06b33f87c779d21619a0d1fcbc6e9ea10cf4d4ea85b3380e8ecdb744237b729a3e838ab470d6128219adbe5ced8c35490ad
6
+ metadata.gz: a926de52b731c63259ba2a6a954e35dd0d94c133d78cfb4bfe8c7d862cf8f5d5f9e6a33ad8b9d63028d9cc9cfa97473f6fca92683c27a219bc5838bcd3d626eb
7
+ data.tar.gz: 112ec835a660537716dd1c94cfa9c152584296a44b857d0161ac3a2dd69c6f2440fd2ccf15542907f2b7a83aeb38a99acce949ad06fb55be62ff20d4687ec55a
data/CHANGELOG.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Changelog
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
+ ## [0.3.4] - 2026-03-06
5
+ ### Changed
6
+ - Reduces object allocations in hot paths for improved performance.
7
+
4
8
  ## [0.3.3] - 2026-03-06
5
9
  ### Changed
6
10
  - 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.4)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
@@ -42,15 +42,18 @@ module ShinyJsonLogic
42
42
  # Returns true if all pairs pass, false otherwise. Raises on :nan or invalid args.
43
43
  def compare_chain(rules, scope_stack)
44
44
  operands = Utils::Array.wrap_nil(rules)
45
- raise Errors::InvalidArguments if operands.length < 2
45
+ n = operands.length
46
+ raise Errors::InvalidArguments if n < 2
46
47
 
47
48
  prev = Engine.call(operands[0], scope_stack)
48
- operands[1..].each do |rule|
49
- curr = Engine.call(rule, scope_stack)
49
+ i = 1
50
+ while i < n
51
+ curr = Engine.call(operands[i], scope_stack)
50
52
  result = compare(prev, curr)
51
53
  raise Errors::NotANumber if result == :nan
52
54
  return false unless yield(result)
53
55
  prev = curr
56
+ i += 1
54
57
  end
55
58
  true
56
59
  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
@@ -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
@@ -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
@@ -19,9 +19,11 @@ module ShinyJsonLogic
19
19
 
20
20
  collection, filter = setup_collection(rules, scope_stack)
21
21
 
22
+ index_scope = { "index" => 0 }
22
23
  on_before(scope_stack)
23
24
  results = collection.each_with_index.each_with_object([]) do |(item, index), acc|
24
- scope_stack.push({ "index" => index }, index: index)
25
+ index_scope["index"] = index
26
+ scope_stack.push(index_scope, index: index)
25
27
  scope_stack.push(item, index: index)
26
28
  begin
27
29
  solved = on_each(item, filter, scope_stack)
@@ -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
@@ -18,9 +18,14 @@ 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
+ reduce_scope = { "current" => nil, "accumulator" => nil }
23
+
21
24
  collection.each_with_index do |item, index|
22
- scope_stack.push({ "index" => index }, index: index)
23
- reduce_scope = { "current" => item, "accumulator" => accumulator }
25
+ index_scope["index"] = index
26
+ reduce_scope["current"] = item
27
+ reduce_scope["accumulator"] = accumulator
28
+ scope_stack.push(index_scope, index: index)
24
29
  scope_stack.push(reduce_scope, index: index)
25
30
  begin
26
31
  accumulator = Engine.call(filter, scope_stack)
@@ -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))
@@ -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
@@ -49,12 +51,10 @@ module ShinyJsonLogic
49
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
+ value.keys.any? { |key| SOLVER_KEYS.include?(key.is_a?(String) ? key : key.to_s) }
58
58
  end
59
59
  end
60
60
  end
@@ -19,41 +19,40 @@ 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]
24
+ @index_stack = [0]
27
25
  end
28
26
 
29
27
  # Push a new scope onto the stack (when entering an iteration)
30
28
  def push(data, index: 0)
31
- stack.push([data, index])
29
+ @data_stack << data
30
+ @index_stack << index
32
31
  end
33
32
 
34
33
  # Pop the top scope (when exiting an iteration)
35
34
  def pop
36
- stack.pop if stack.size > 1
35
+ if @data_stack.size > 1
36
+ @data_stack.pop
37
+ @index_stack.pop
38
+ end
37
39
  end
38
40
 
39
41
  # Returns the current scope's data (top of stack)
40
42
  def current
41
- stack.last[0]
43
+ @data_stack.last
42
44
  end
43
45
 
44
46
  # Resolve a value by going up n levels and then accessing keys
45
- #
47
+ #
46
48
  # @param levels [Integer] number of levels to go up (0 = current, 1 = parent, etc.)
47
49
  # @param keys [Array] keys to dig into after reaching the target scope
48
50
  # @return [Object] the resolved value
49
51
  def resolve(levels, *keys)
50
- target_index = stack.size - 1 - levels
52
+ target_index = @data_stack.size - 1 - levels
51
53
  return nil if target_index < 0
52
54
 
53
- scope = stack[target_index]
54
- return nil unless scope
55
-
56
- data = scope[0]
55
+ data = @data_stack[target_index]
57
56
 
58
57
  if keys.empty?
59
58
  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,6 +15,7 @@ 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)
17
19
 
18
20
  wrap(object)
19
21
  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.4"
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.4
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: []