shiny_json_logic 0.3.2 → 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 +4 -4
- data/.github/workflows/ci.yml +5 -3
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/lib/shiny_json_logic/comparisons/comparable.rb +6 -3
- data/lib/shiny_json_logic/engine.rb +6 -4
- data/lib/shiny_json_logic/numericals/min_max_collection.rb +2 -5
- data/lib/shiny_json_logic/numericals/with_error_handling.rb +1 -1
- data/lib/shiny_json_logic/operations/if.rb +7 -3
- data/lib/shiny_json_logic/operations/iterable/base.rb +3 -1
- data/lib/shiny_json_logic/operations/missing.rb +13 -2
- data/lib/shiny_json_logic/operations/reduce.rb +7 -2
- data/lib/shiny_json_logic/operations/strict_different.rb +6 -3
- data/lib/shiny_json_logic/operations/strict_equal.rb +6 -3
- data/lib/shiny_json_logic/operations/val.rb +9 -12
- data/lib/shiny_json_logic/operations/var.rb +8 -10
- data/lib/shiny_json_logic/operator_solver.rb +4 -4
- data/lib/shiny_json_logic/scope_stack.rb +16 -25
- data/lib/shiny_json_logic/truthy.rb +6 -7
- data/lib/shiny_json_logic/utils/array.rb +2 -0
- data/lib/shiny_json_logic/utils/data_hash.rb +1 -3
- data/lib/shiny_json_logic/utils/hash_fetch.rb +30 -0
- data/lib/shiny_json_logic/utils/indifferent_hash.rb +72 -0
- data/lib/shiny_json_logic/version.rb +1 -1
- data/lib/shiny_json_logic.rb +1 -18
- data/shiny_json_logic.gemspec +4 -5
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf0275f81efb49af13061d1c54c2dc273fb4686ad647a43f3ba4eafb069176f5
|
|
4
|
+
data.tar.gz: d411aa52d5d3a235b5a9c783909acac800e10a620e0294f572ccbca9d7421223
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a926de52b731c63259ba2a6a954e35dd0d94c133d78cfb4bfe8c7d862cf8f5d5f9e6a33ad8b9d63028d9cc9cfa97473f6fca92683c27a219bc5838bcd3d626eb
|
|
7
|
+
data.tar.gz: 112ec835a660537716dd1c94cfa9c152584296a44b857d0161ac3a2dd69c6f2440fd2ccf15542907f2b7a83aeb38a99acce949ad06fb55be62ff20d4687ec55a
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
strategy:
|
|
13
13
|
fail-fast: false
|
|
14
14
|
matrix:
|
|
15
|
-
ruby: ["2.7", "3.0", "3.2", "3.3", "4.0"]
|
|
15
|
+
ruby: ["2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "4.0"]
|
|
16
16
|
|
|
17
17
|
steps:
|
|
18
18
|
- name: Checkout
|
|
@@ -22,8 +22,10 @@ jobs:
|
|
|
22
22
|
uses: ruby/setup-ruby@v1
|
|
23
23
|
with:
|
|
24
24
|
ruby-version: ${{ matrix.ruby }}
|
|
25
|
-
bundler:
|
|
26
|
-
|
|
25
|
+
bundler: latest
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: bundle install
|
|
27
29
|
|
|
28
30
|
- name: Run tests
|
|
29
31
|
run: bundle exec rspec
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
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
|
+
|
|
8
|
+
## [0.3.3] - 2026-03-06
|
|
9
|
+
### Changed
|
|
10
|
+
- Refactors internal architecture to lookup operations with a helper instead of running a normalization pass before calculations, thus improving performance a lot.
|
|
11
|
+
- Removes double op lookup on engine.rb
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Includes `Utils::HashFetch` to allow fetching with any key type
|
|
15
|
+
|
|
4
16
|
## [0.3.2] - 2026-02-28
|
|
5
17
|
### Changed
|
|
6
18
|
- Refactors scope stack as an array of arrays in order to improve performance
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
[](https://badge.fury.io/rb/shiny_json_logic)
|
|
5
|
-

|
|
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.
|
|
18
|
+
- 🕰️ **Ruby 2.6+ 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
|
|
|
@@ -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
|
-
|
|
45
|
+
n = operands.length
|
|
46
|
+
raise Errors::InvalidArguments if n < 2
|
|
46
47
|
|
|
47
48
|
prev = Engine.call(operands[0], scope_stack)
|
|
48
|
-
|
|
49
|
-
|
|
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,12 +15,14 @@ module ShinyJsonLogic
|
|
|
15
15
|
|
|
16
16
|
raise Errors::UnknownOperator if rule.size > 1
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
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]
|
|
23
|
+
raise Errors::UnknownOperator unless op
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
op.call(args, scope_stack)
|
|
24
26
|
elsif rule.is_a?(Array)
|
|
25
27
|
rule.map { |val| call(val, scope_stack) }
|
|
26
28
|
else
|
|
@@ -20,11 +20,8 @@ module ShinyJsonLogic
|
|
|
20
20
|
return Utils::Array.wrap_nil(evaluated)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
@@ -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.
|
|
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
|
-
|
|
24
|
+
return Engine.call(value_rule, scope_stack) if Truthy.call(condition_result)
|
|
21
25
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
reduce_scope
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "shiny_json_logic/operations/base"
|
|
4
4
|
require "shiny_json_logic/utils/data_hash"
|
|
5
|
+
require "shiny_json_logic/utils/hash_fetch"
|
|
5
6
|
|
|
6
7
|
module ShinyJsonLogic
|
|
7
8
|
module Operations
|
|
@@ -19,9 +20,13 @@ module ShinyJsonLogic
|
|
|
19
20
|
|
|
20
21
|
if first_key.is_a?(Array) && scope_stack
|
|
21
22
|
level_indicator = first_key.first.to_i
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
25
30
|
|
|
26
31
|
levels = level_indicator.abs
|
|
27
32
|
return Utils::DataHash.wrap(scope_stack.resolve(levels, *evaluated_keys))
|
|
@@ -39,15 +44,7 @@ module ShinyJsonLogic
|
|
|
39
44
|
|
|
40
45
|
keys.reduce(data) do |obj, key|
|
|
41
46
|
return nil if obj.nil?
|
|
42
|
-
|
|
43
|
-
if obj.is_a?(Hash)
|
|
44
|
-
obj[key.to_s]
|
|
45
|
-
elsif obj.is_a?(Array)
|
|
46
|
-
index = key.is_a?(String) ? key.to_i : key
|
|
47
|
-
obj[index]
|
|
48
|
-
else
|
|
49
|
-
nil
|
|
50
|
-
end
|
|
47
|
+
Utils::HashFetch.fetch(obj, key.to_s)
|
|
51
48
|
end
|
|
52
49
|
end
|
|
53
50
|
private_class_method :dig_value
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "shiny_json_logic/truthy"
|
|
4
4
|
require "shiny_json_logic/operations/base"
|
|
5
5
|
require "shiny_json_logic/utils/data_hash"
|
|
6
|
+
require "shiny_json_logic/utils/hash_fetch"
|
|
6
7
|
|
|
7
8
|
module ShinyJsonLogic
|
|
8
9
|
module Operations
|
|
@@ -27,18 +28,15 @@ module ShinyJsonLogic
|
|
|
27
28
|
def self.fetch_value(obj, key)
|
|
28
29
|
return nil if obj.nil?
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
key_s = key.to_s
|
|
32
|
+
# Fast path: no dot notation, single key lookup
|
|
33
|
+
unless key_s.include?(".")
|
|
34
|
+
return Utils::HashFetch.fetch(obj, key_s)
|
|
35
|
+
end
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
key_s.split(".").reduce(obj) do |current, k|
|
|
33
38
|
return nil if current.nil?
|
|
34
|
-
|
|
35
|
-
if current.is_a?(Hash)
|
|
36
|
-
current[k]
|
|
37
|
-
elsif current.is_a?(Array)
|
|
38
|
-
current[k.to_i]
|
|
39
|
-
else
|
|
40
|
-
nil
|
|
41
|
-
end
|
|
39
|
+
Utils::HashFetch.fetch(current, k)
|
|
42
40
|
end
|
|
43
41
|
end
|
|
44
42
|
private_class_method :fetch_value
|
|
@@ -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
|
-
|
|
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|
|
|
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
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "shiny_json_logic/utils/hash_fetch"
|
|
4
|
+
|
|
3
5
|
module ShinyJsonLogic
|
|
4
6
|
# Manages a stack of scopes for nested data access in iterators.
|
|
5
7
|
#
|
|
@@ -17,41 +19,40 @@ module ShinyJsonLogic
|
|
|
17
19
|
# so no indifferent access is needed here.
|
|
18
20
|
#
|
|
19
21
|
class ScopeStack
|
|
20
|
-
attr_reader :stack
|
|
21
|
-
|
|
22
22
|
def initialize(root_data)
|
|
23
|
-
@
|
|
24
|
-
@
|
|
23
|
+
@data_stack = [root_data]
|
|
24
|
+
@index_stack = [0]
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# Push a new scope onto the stack (when entering an iteration)
|
|
28
28
|
def push(data, index: 0)
|
|
29
|
-
|
|
29
|
+
@data_stack << data
|
|
30
|
+
@index_stack << index
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
# Pop the top scope (when exiting an iteration)
|
|
33
34
|
def pop
|
|
34
|
-
|
|
35
|
+
if @data_stack.size > 1
|
|
36
|
+
@data_stack.pop
|
|
37
|
+
@index_stack.pop
|
|
38
|
+
end
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
# Returns the current scope's data (top of stack)
|
|
38
42
|
def current
|
|
39
|
-
|
|
43
|
+
@data_stack.last
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
# Resolve a value by going up n levels and then accessing keys
|
|
43
|
-
#
|
|
47
|
+
#
|
|
44
48
|
# @param levels [Integer] number of levels to go up (0 = current, 1 = parent, etc.)
|
|
45
49
|
# @param keys [Array] keys to dig into after reaching the target scope
|
|
46
50
|
# @return [Object] the resolved value
|
|
47
51
|
def resolve(levels, *keys)
|
|
48
|
-
target_index =
|
|
52
|
+
target_index = @data_stack.size - 1 - levels
|
|
49
53
|
return nil if target_index < 0
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
return nil unless scope
|
|
53
|
-
|
|
54
|
-
data = scope[0]
|
|
55
|
+
data = @data_stack[target_index]
|
|
55
56
|
|
|
56
57
|
if keys.empty?
|
|
57
58
|
data
|
|
@@ -64,20 +65,10 @@ module ShinyJsonLogic
|
|
|
64
65
|
|
|
65
66
|
def dig_value(data, keys)
|
|
66
67
|
return nil if data.nil?
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
keys.reduce(data) do |obj, key|
|
|
69
70
|
return nil if obj.nil?
|
|
70
|
-
|
|
71
|
-
if obj.is_a?(Hash)
|
|
72
|
-
# Normalize key to string for lookup
|
|
73
|
-
obj[key.to_s]
|
|
74
|
-
elsif obj.is_a?(Array)
|
|
75
|
-
# Convert string keys to integers for arrays
|
|
76
|
-
index = key.is_a?(String) ? key.to_i : key
|
|
77
|
-
obj[index]
|
|
78
|
-
else
|
|
79
|
-
nil
|
|
80
|
-
end
|
|
71
|
+
Utils::HashFetch.fetch(obj, key.to_s)
|
|
81
72
|
end
|
|
82
73
|
end
|
|
83
74
|
end
|
|
@@ -6,13 +6,12 @@ module ShinyJsonLogic
|
|
|
6
6
|
module Truthy
|
|
7
7
|
def self.call(subject)
|
|
8
8
|
case subject
|
|
9
|
-
when true, false
|
|
10
|
-
when Numeric
|
|
11
|
-
when String
|
|
12
|
-
when Array
|
|
13
|
-
when
|
|
14
|
-
|
|
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,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShinyJsonLogic
|
|
4
|
+
module Utils
|
|
5
|
+
module HashFetch
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Fetches a value from a Hash or Array using a string key, with symbol fallback.
|
|
9
|
+
#
|
|
10
|
+
# For Hash: tries string key first, then symbol key. Uses key? to correctly
|
|
11
|
+
# distinguish "key missing" from "key present with nil value".
|
|
12
|
+
# For Array: converts key to integer index.
|
|
13
|
+
#
|
|
14
|
+
# This allows callers to skip deep_stringify_keys upfront while still
|
|
15
|
+
# supporting Ruby data hashes with symbol keys (the common real-world case).
|
|
16
|
+
def fetch(obj, key_s)
|
|
17
|
+
if obj.is_a?(::Hash)
|
|
18
|
+
if obj.key?(key_s)
|
|
19
|
+
obj[key_s]
|
|
20
|
+
else
|
|
21
|
+
sym = key_s.to_sym
|
|
22
|
+
obj.key?(sym) ? obj[sym] : nil
|
|
23
|
+
end
|
|
24
|
+
elsif obj.is_a?(::Array)
|
|
25
|
+
obj[key_s.to_i]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
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
|
data/lib/shiny_json_logic.rb
CHANGED
|
@@ -11,26 +11,9 @@ require "shiny_json_logic/scope_stack"
|
|
|
11
11
|
|
|
12
12
|
module ShinyJsonLogic
|
|
13
13
|
def self.apply(rule, data = {})
|
|
14
|
-
|
|
15
|
-
scope_stack = ScopeStack.new(normalized_data)
|
|
14
|
+
scope_stack = ScopeStack.new(data || {})
|
|
16
15
|
Engine.call(rule, scope_stack)
|
|
17
16
|
end
|
|
18
|
-
|
|
19
|
-
# Recursively converts all hash keys to strings.
|
|
20
|
-
# Fast path: if all keys are already strings, skip the copy.
|
|
21
|
-
def self.deep_stringify_keys(obj)
|
|
22
|
-
case obj
|
|
23
|
-
when Hash
|
|
24
|
-
return obj if obj.keys.all? { |k| k.is_a?(String) }
|
|
25
|
-
obj.each_with_object({}) do |(key, value), result|
|
|
26
|
-
result[key.to_s] = deep_stringify_keys(value)
|
|
27
|
-
end
|
|
28
|
-
when Array
|
|
29
|
-
obj.map { |item| deep_stringify_keys(item) }
|
|
30
|
-
else
|
|
31
|
-
obj
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
17
|
end
|
|
35
18
|
|
|
36
19
|
JsonLogic = ShinyJsonLogic
|
data/shiny_json_logic.gemspec
CHANGED
|
@@ -7,17 +7,16 @@ Gem::Specification.new do |spec|
|
|
|
7
7
|
spec.name = "shiny_json_logic"
|
|
8
8
|
spec.version = ShinyJsonLogic::VERSION
|
|
9
9
|
spec.authors = ["Luis Moyano"]
|
|
10
|
-
spec.email = ["
|
|
11
|
-
|
|
12
|
-
spec.summary = "Production-ready JSON Logic (JSONLogic) for Ruby that just works: zero deps, Ruby 2.7+, high spec alignment."
|
|
10
|
+
spec.email = [""]
|
|
11
|
+
spec.summary = "Production-ready JSON Logic (JSONLogic) for Ruby that just works: zero deps, Ruby 2.4+, full spec alignment (100%)."
|
|
13
12
|
spec.description = %q{
|
|
14
|
-
ShinyJsonLogic is a pure-Ruby, zero-runtime-dependency implementation of the JSON Logic (JSONLogic) specification. Ruby 2.
|
|
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.
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
spec.homepage = "https://jsonlogicruby.com"
|
|
18
17
|
spec.license = "MIT"
|
|
19
18
|
|
|
20
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
|
19
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
|
21
20
|
|
|
22
21
|
spec.metadata = {
|
|
23
22
|
"homepage_uri" => spec.homepage,
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shiny_json_logic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Luis Moyano
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-03-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -95,13 +95,13 @@ 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.
|
|
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
|
|
102
102
|
concise overview of JsonLogic in the ruby ecosystem.\n "
|
|
103
103
|
email:
|
|
104
|
-
-
|
|
104
|
+
- ''
|
|
105
105
|
executables: []
|
|
106
106
|
extensions: []
|
|
107
107
|
extra_rdoc_files: []
|
|
@@ -178,6 +178,8 @@ files:
|
|
|
178
178
|
- lib/shiny_json_logic/truthy.rb
|
|
179
179
|
- lib/shiny_json_logic/utils/array.rb
|
|
180
180
|
- lib/shiny_json_logic/utils/data_hash.rb
|
|
181
|
+
- lib/shiny_json_logic/utils/hash_fetch.rb
|
|
182
|
+
- lib/shiny_json_logic/utils/indifferent_hash.rb
|
|
181
183
|
- lib/shiny_json_logic/version.rb
|
|
182
184
|
- results/ruby.json
|
|
183
185
|
- shiny_json_logic.gemspec
|
|
@@ -198,7 +200,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
198
200
|
requirements:
|
|
199
201
|
- - ">="
|
|
200
202
|
- !ruby/object:Gem::Version
|
|
201
|
-
version: 2.
|
|
203
|
+
version: 2.4.0
|
|
202
204
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
203
205
|
requirements:
|
|
204
206
|
- - ">="
|
|
@@ -209,5 +211,5 @@ rubygems_version: 3.1.6
|
|
|
209
211
|
signing_key:
|
|
210
212
|
specification_version: 4
|
|
211
213
|
summary: 'Production-ready JSON Logic (JSONLogic) for Ruby that just works: zero deps,
|
|
212
|
-
Ruby 2.
|
|
214
|
+
Ruby 2.4+, full spec alignment (100%).'
|
|
213
215
|
test_files: []
|