shiny_json_logic 0.2.8 → 0.2.9

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile.lock +1 -1
  4. data/bin/test.sh +57 -0
  5. data/lib/shiny_json_logic/engine.rb +5 -25
  6. data/lib/shiny_json_logic/errors/base.rb +1 -5
  7. data/lib/shiny_json_logic/errors/invalid_arguments.rb +11 -0
  8. data/lib/shiny_json_logic/errors/not_a_number.rb +11 -0
  9. data/lib/shiny_json_logic/errors/unknown_operator.rb +11 -0
  10. data/lib/shiny_json_logic/numericals/min_max_collection.rb +31 -0
  11. data/lib/shiny_json_logic/numericals/with_error_handling.rb +4 -10
  12. data/lib/shiny_json_logic/operations/addition.rb +1 -3
  13. data/lib/shiny_json_logic/operations/and.rb +1 -3
  14. data/lib/shiny_json_logic/operations/base.rb +12 -25
  15. data/lib/shiny_json_logic/operations/coalesce.rb +1 -3
  16. data/lib/shiny_json_logic/operations/concatenation.rb +1 -3
  17. data/lib/shiny_json_logic/operations/different.rb +1 -3
  18. data/lib/shiny_json_logic/operations/division.rb +1 -3
  19. data/lib/shiny_json_logic/operations/double_not.rb +1 -3
  20. data/lib/shiny_json_logic/operations/equal.rb +1 -3
  21. data/lib/shiny_json_logic/operations/exists.rb +1 -3
  22. data/lib/shiny_json_logic/operations/filter.rb +1 -2
  23. data/lib/shiny_json_logic/operations/greater.rb +1 -3
  24. data/lib/shiny_json_logic/operations/greater_equal.rb +1 -3
  25. data/lib/shiny_json_logic/operations/if.rb +6 -18
  26. data/lib/shiny_json_logic/operations/inclusion.rb +1 -3
  27. data/lib/shiny_json_logic/operations/iterable/base.rb +16 -32
  28. data/lib/shiny_json_logic/operations/max.rb +4 -9
  29. data/lib/shiny_json_logic/operations/merge.rb +1 -3
  30. data/lib/shiny_json_logic/operations/min.rb +4 -9
  31. data/lib/shiny_json_logic/operations/missing.rb +1 -3
  32. data/lib/shiny_json_logic/operations/missing_some.rb +2 -5
  33. data/lib/shiny_json_logic/operations/modulo.rb +3 -5
  34. data/lib/shiny_json_logic/operations/not.rb +1 -3
  35. data/lib/shiny_json_logic/operations/or.rb +1 -3
  36. data/lib/shiny_json_logic/operations/preserve.rb +8 -7
  37. data/lib/shiny_json_logic/operations/product.rb +1 -3
  38. data/lib/shiny_json_logic/operations/reduce.rb +5 -7
  39. data/lib/shiny_json_logic/operations/smaller.rb +1 -3
  40. data/lib/shiny_json_logic/operations/smaller_equal.rb +1 -3
  41. data/lib/shiny_json_logic/operations/strict_different.rb +1 -3
  42. data/lib/shiny_json_logic/operations/strict_equal.rb +1 -3
  43. data/lib/shiny_json_logic/operations/substring.rb +1 -3
  44. data/lib/shiny_json_logic/operations/subtraction.rb +3 -5
  45. data/lib/shiny_json_logic/operations/throw.rb +3 -4
  46. data/lib/shiny_json_logic/operations/try.rb +19 -24
  47. data/lib/shiny_json_logic/operations/val.rb +1 -3
  48. data/lib/shiny_json_logic/operations/var.rb +1 -3
  49. data/lib/shiny_json_logic/version.rb +1 -1
  50. data/lib/shiny_json_logic.rb +34 -4
  51. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e05288f3f43187927c102d03ce0cd653bf8762dd7e0adc40b29d27e0c4f7984f
4
- data.tar.gz: 4c6e781dec321b4f928b9b1d786c708ffe39662cc22676cc5ab7d8228dff8aa8
3
+ metadata.gz: ecf7aea99599023d6adf2af12bbb99fe8a40d9d8bcce1c2410eb4a49337b08b6
4
+ data.tar.gz: 73944111a179b14d04e401351519c46400294b39e95571b3f744225915f759a6
5
5
  SHA512:
6
- metadata.gz: '0459cef93e4ce203e8ea1022c7bf7305aef736551e56bf3e8c47326ddcf95481232c788f5b3d5c96f25137df730b6cb5180ac3b7aadd696a1c4a0d9bea7e0aa3'
7
- data.tar.gz: d3801f6f5cc0a0df664d829e352c934345305ce264a57bd39a7dd3f7e637963d402a0c7676e8c4f9fffa76fea7dffe6cca04d655bc38b79dfc781877faa616d5
6
+ metadata.gz: 8676f95bd3ce898099859c0828fd56f0be608555ca89ecc3e133ffd4bedd435820c9ce58e15ecd2f4f44521514c1ddfbed3c8d235c4ca37acd1c87dc93bafc5c
7
+ data.tar.gz: 91d46fb710614bcf75ae1a198f21f9fabd06875f89c48224413c36a04668894c5ee2276374d1a0f6ca9a7d7f858c4f68b737386ffd2b6c9cb7e819b4b75a6e5a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.9] - 2026-02-08
6
+ ### Added
7
+ - New specific error classes: `Errors::InvalidArguments`, `Errors::NotANumber`, `Errors::UnknownOperator`
8
+ - All inherit from `Errors::Base`, so existing `rescue Errors::Base` will continue to work
9
+ ### Changed
10
+ - Refactors max/min operators to use shared MinMaxCollection module
11
+ - Eliminates code duplication between max.rb and min.rb
12
+
5
13
  ## [0.2.8] - 2026-02-07
6
14
  ### Changed
7
15
  - Improves error handling in try/throw/reduce operators
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shiny_json_logic (0.2.8)
4
+ shiny_json_logic (0.2.9)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/bin/test.sh CHANGED
@@ -7,8 +7,11 @@ cd "$ROOT_DIR"
7
7
  # Config
8
8
  COMPAT_REF="${COMPAT_REF:-main}"
9
9
  COMPAT_REPO="${COMPAT_REPO:-json-logic/compat-tables}"
10
+ OFFICIAL_REF="${OFFICIAL_REF:-main}"
11
+ OFFICIAL_REPO="${OFFICIAL_REPO:-json-logic/.github}"
10
12
  TMP_DIR="${TMP_DIR:-$ROOT_DIR/tmp}"
11
13
  SUITES_DIR="$TMP_DIR/compat-suites"
14
+ OFFICIAL_DIR="$TMP_DIR/official-tests"
12
15
 
13
16
  # Helpers
14
17
  log() { printf "\n\033[1m%s\033[0m\n" "$*"; }
@@ -37,6 +40,22 @@ curl -fsSL "$ARCHIVE_URL" \
37
40
 
38
41
  log "Compat suites ready at: $SUITES_DIR"
39
42
 
43
+ log "Fetching official tests from GitHub: $OFFICIAL_REPO@$OFFICIAL_REF"
44
+
45
+ rm -rf "$OFFICIAL_DIR"
46
+ mkdir -p "$OFFICIAL_DIR"
47
+
48
+ OFFICIAL_URL="https://codeload.github.com/${OFFICIAL_REPO}/tar.gz/${OFFICIAL_REF}"
49
+
50
+ # Extract only: .github-<ref>/tests -> tmp/official-tests
51
+ curl -fsSL "$OFFICIAL_URL" \
52
+ | tar -xz \
53
+ --strip-components=2 \
54
+ -C "$OFFICIAL_DIR" \
55
+ ".github-${OFFICIAL_REF}/tests" 2>/dev/null \
56
+
57
+ log "Official tests ready at: $OFFICIAL_DIR"
58
+
40
59
  log "Running compatibility suite"
41
60
  COMPAT_OUT="$TMP_DIR/compat-rspec.out"
42
61
 
@@ -75,3 +94,41 @@ cat > results/ruby.json <<EOF
75
94
  EOF
76
95
 
77
96
  echo "Compat: ${PASSED}/${TOTAL} (failures: ${FAILURES}, exit: ${COMPAT_EXIT})"
97
+
98
+ # ---- Official tests (informational only, does not affect badge) ----
99
+ log "Running official tests (informational)"
100
+ OFFICIAL_OUT="$TMP_DIR/official-rspec.out"
101
+
102
+ set +e
103
+ OFFICIAL_TESTS_DIR="$OFFICIAL_DIR" bundle exec rspec spec/official_spec.rb | tee "$OFFICIAL_OUT"
104
+ OFFICIAL_EXIT=${PIPESTATUS[0]}
105
+ set -e
106
+
107
+ # Parse totals from RSpec summary
108
+ OFFICIAL_TOTAL=$(ruby -e '
109
+ s = STDIN.read
110
+ m = s.match(/(\d+)\s+examples?,\s+(\d+)\s+failures?/)
111
+ if m
112
+ puts m[1]
113
+ else
114
+ puts "0"
115
+ end
116
+ ' < "$OFFICIAL_OUT")
117
+
118
+ OFFICIAL_FAILURES=$(ruby -e '
119
+ s = STDIN.read
120
+ m = s.match(/(\d+)\s+examples?,\s+(\d+)\s+failures?/)
121
+ if m
122
+ puts m[2]
123
+ else
124
+ puts "0"
125
+ end
126
+ ' < "$OFFICIAL_OUT")
127
+
128
+ OFFICIAL_PASSED=$((OFFICIAL_TOTAL - OFFICIAL_FAILURES))
129
+
130
+ echo "Official: ${OFFICIAL_PASSED}/${OFFICIAL_TOTAL} (failures: ${OFFICIAL_FAILURES}, exit: ${OFFICIAL_EXIT})"
131
+ echo ""
132
+ echo "=== Summary ==="
133
+ echo "Compat (badge): ${PASSED}/${TOTAL}"
134
+ echo "Official (info): ${OFFICIAL_PASSED}/${OFFICIAL_TOTAL}"
@@ -1,19 +1,12 @@
1
1
  require "core_ext/array"
2
2
  require "core_ext/hash"
3
3
  require "shiny_json_logic/operator_solver"
4
- require "shiny_json_logic/scope_stack"
5
4
 
6
5
  module ShinyJsonLogic
7
6
  class Engine
8
- attr_reader :errors
9
-
10
- # Initialize with either:
11
- # - Engine.new(rule, data) - creates a new scope_stack with data as root
12
- # - Engine.new(rule, scope_stack: existing_stack) - uses existing scope_stack
13
- def initialize(rule, data = nil, scope_stack: nil)
7
+ def initialize(rule, scope_stack)
14
8
  @rule = rule
15
- @errors = []
16
- @scope_stack = scope_stack || ScopeStack.new(data || {})
9
+ @scope_stack = scope_stack
17
10
  end
18
11
 
19
12
  def call(rule = self.rule)
@@ -21,9 +14,9 @@ module ShinyJsonLogic
21
14
  return rule if rule.empty?
22
15
 
23
16
  operation, args = rule.to_a.first
24
- return rule unless operations.solvers.key?(operation)
17
+ return rule unless operations.key?(operation)
25
18
 
26
- solve(operation, args)
19
+ operations.fetch(operation).new(args, scope_stack).call
27
20
  elsif rule.is_a?(Array)
28
21
  rule.map { |val| call(val) }
29
22
  else
@@ -34,22 +27,9 @@ module ShinyJsonLogic
34
27
  private
35
28
 
36
29
  attr_reader :rule, :scope_stack
37
- attr_writer :errors
38
-
39
- def solve(operation, args)
40
- context = {
41
- "rules" => args,
42
- "errors" => errors,
43
- "scope_stack" => scope_stack
44
- }
45
- result, errors = operations.solvers.fetch(operation).new(context).call.values_at("result", "errors")
46
- self.errors = [*self.errors, *errors].uniq
47
-
48
- result
49
- end
50
30
 
51
31
  def operations
52
- @operations ||= OperatorSolver.new
32
+ @operations ||= OperatorSolver.new.solvers
53
33
  end
54
34
  end
55
35
  end
@@ -1,15 +1,11 @@
1
- require "securerandom"
2
-
3
1
  module ShinyJsonLogic
4
2
  module Errors
5
3
  class Base < StandardError
6
- attr_reader :type, :id
7
- attr_accessor :panic
4
+ attr_reader :type
8
5
 
9
6
  def initialize(type: nil)
10
7
  super(type)
11
8
  @type = type
12
- @id = "shiny_error_#{SecureRandom.uuid}"
13
9
  end
14
10
 
15
11
  def payload
@@ -0,0 +1,11 @@
1
+ require "shiny_json_logic/errors/base"
2
+
3
+ module ShinyJsonLogic
4
+ module Errors
5
+ class InvalidArguments < Base
6
+ def initialize
7
+ super(type: "Invalid Arguments")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require "shiny_json_logic/errors/base"
2
+
3
+ module ShinyJsonLogic
4
+ module Errors
5
+ class NotANumber < Base
6
+ def initialize
7
+ super(type: "NaN")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ require "shiny_json_logic/errors/base"
2
+
3
+ module ShinyJsonLogic
4
+ module Errors
5
+ class UnknownOperator < Base
6
+ def initialize
7
+ super(type: "Unknown Operator")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ require "shiny_json_logic/errors/invalid_arguments"
2
+
3
+ module ShinyJsonLogic
4
+ module Numericals
5
+ module MinMaxCollection
6
+ private
7
+
8
+ def collect_numeric_values
9
+ values = collect_values
10
+ raise Errors::InvalidArguments if values.empty?
11
+ raise Errors::InvalidArguments unless values.all? { |v| v.is_a?(Numeric) }
12
+ values
13
+ end
14
+
15
+ def collect_values
16
+ result = []
17
+ Array.wrap_nil(rules).each do |rule|
18
+ evaluated = evaluate(rule)
19
+ # If rule was an operation (Hash), expand the result array
20
+ # If rule was a literal array, it's invalid (will fail numeric check)
21
+ if operation?(rule)
22
+ Array.wrap_nil(evaluated).each { |val| result << val }
23
+ else
24
+ result << evaluated
25
+ end
26
+ end
27
+ result
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,4 +1,6 @@
1
1
  require "shiny_json_logic/errors/base"
2
+ require "shiny_json_logic/errors/not_a_number"
3
+ require "shiny_json_logic/errors/invalid_arguments"
2
4
 
3
5
  module ShinyJsonLogic
4
6
  module Numericals
@@ -14,20 +16,12 @@ module ShinyJsonLogic
14
16
  end
15
17
 
16
18
  def handle_nan
17
- error = ShinyJsonLogic::Errors::Base.new(type: "NaN")
18
- self.errors << error
19
- error.id
19
+ raise Errors::NotANumber
20
20
  end
21
21
 
22
22
  def handle_invalid_args
23
- error = ShinyJsonLogic::Errors::Base.new(type: "Invalid Arguments")
24
- self.errors << error
25
- error.id
23
+ raise Errors::InvalidArguments
26
24
  end
27
-
28
- # Alias for backward compatibility
29
- alias_method :handle_invalid_operand, :handle_nan
30
- alias_method :handle_no_operators, :handle_invalid_args
31
25
  end
32
26
  end
33
27
  end
@@ -8,9 +8,7 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  include Numericals::Numerify
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  safe_arithmetic do
15
13
  result = 0.0
16
14
  count = 0
@@ -8,9 +8,7 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  raise_on_dynamic_args!
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  return handle_invalid_args if dynamic_args?
15
13
  return handle_invalid_args unless rules.is_a?(Array)
16
14
  return false if rules.empty?
@@ -3,40 +3,28 @@ require "shiny_json_logic/truthy"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Base
6
- def initialize(context)
7
- @context = context
8
- @rules, @errors, @scope_stack = @context.values_at("rules", "errors", "scope_stack")
9
- @dynamic_args = operation?(@rules)
10
- @rules = pre_process(@rules)
6
+ def initialize(rules, scope_stack)
7
+ @scope_stack = scope_stack
8
+ @dynamic_args = operation?(rules)
9
+ @rules = pre_process(rules)
11
10
  end
12
11
 
13
12
  def call
14
- deliver run
13
+ raise NotImplementedError
15
14
  end
16
15
 
17
16
  protected
18
17
 
19
- attr_reader :context, :scope_stack
20
- attr_accessor :rules, :errors
18
+ attr_reader :scope_stack
19
+ attr_accessor :rules
21
20
 
22
21
  # Access current data through scope_stack
23
22
  def data
24
23
  scope_stack.current
25
24
  end
26
25
 
27
- def run
28
- raise NotImplementedError
29
- end
30
-
31
- def deliver(result = nil)
32
- {"result" => result, "errors" => self.errors}
33
- end
34
-
35
26
  def evaluate(rule)
36
- engine = Engine.new(rule, scope_stack: scope_stack)
37
- result = engine.call
38
- self.errors = [*errors, *engine.errors].uniq
39
- result
27
+ Engine.new(rule, scope_stack).call
40
28
  end
41
29
 
42
30
  def dynamic_args?
@@ -54,15 +42,14 @@ module ShinyJsonLogic
54
42
  private
55
43
 
56
44
  def pre_process(rules)
57
- if operation?(rules)
58
- evaluate(rules)
59
- else
60
- rules
61
- end
45
+ return evaluate(rules) if operation?(rules)
46
+
47
+ rules
62
48
  end
63
49
 
64
50
  def operation?(value)
65
51
  return false unless value.is_a?(Hash) && !value.empty?
52
+
66
53
  OperatorSolver.new.operation?(value)
67
54
  end
68
55
  end
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Coalesce < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  rules.each do |rule|
10
8
  result = evaluate(rule)
11
9
  return result unless result.nil?
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Concatenation < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  result = []
10
8
  Array.wrap_nil(rules).each do |rule|
11
9
  evaluated = evaluate(rule)
@@ -9,9 +9,7 @@ module ShinyJsonLogic
9
9
  include Numericals::Numerify
10
10
  raise_on_dynamic_args!
11
11
 
12
- protected
13
-
14
- def run
12
+ def call
15
13
  return handle_invalid_args if dynamic_args?
16
14
  operands = Array.wrap_nil(rules)
17
15
  return handle_invalid_args if operands.length < 2
@@ -8,9 +8,7 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  include Numericals::Numerify
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  operands = Array.wrap_nil(rules)
15
13
  return handle_invalid_args if operands.empty?
16
14
 
@@ -4,9 +4,7 @@ require "shiny_json_logic/truthy"
4
4
  module ShinyJsonLogic
5
5
  module Operations
6
6
  class DoubleNot < Base
7
- protected
8
-
9
- def run
7
+ def call
10
8
  value = Array.wrap_nil(rules).first
11
9
  !!Truthy.call(evaluate(value))
12
10
  end
@@ -9,9 +9,7 @@ module ShinyJsonLogic
9
9
  include Numericals::Numerify
10
10
  raise_on_dynamic_args!
11
11
 
12
- protected
13
-
14
- def run
12
+ def call
15
13
  return handle_invalid_args if dynamic_args?
16
14
  operands = Array.wrap_nil(rules)
17
15
  return handle_invalid_args if operands.length < 2
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Exists < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  current = data
10
8
 
11
9
  Array.wrap_nil(rules).each do |rule|
@@ -10,8 +10,7 @@ module ShinyJsonLogic
10
10
  private
11
11
 
12
12
  def on_each(item)
13
- engine = Engine.new(filter, scope_stack: scope_stack)
14
- [Truthy.call(engine.call) ? item : nil, engine]
13
+ Truthy.call(Engine.new(filter, scope_stack).call) ? item : nil
15
14
  end
16
15
 
17
16
  def on_after(results)
@@ -9,9 +9,7 @@ module ShinyJsonLogic
9
9
  include Numericals::Numerify
10
10
  raise_on_dynamic_args!
11
11
 
12
- protected
13
-
14
- def run
12
+ def call
15
13
  return handle_invalid_args if dynamic_args?
16
14
  operands = Array.wrap_nil(rules)
17
15
  return handle_invalid_args if operands.length < 2
@@ -9,9 +9,7 @@ module ShinyJsonLogic
9
9
  include Numericals::Numerify
10
10
  raise_on_dynamic_args!
11
11
 
12
- protected
13
-
14
- def run
12
+ def call
15
13
  return handle_invalid_args if dynamic_args?
16
14
  operands = Array.wrap_nil(rules)
17
15
  return handle_invalid_args if operands.length < 2
@@ -8,26 +8,21 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
 
10
10
  # Skip pre_process - spec requires static array, dynamic args should error
11
- def initialize(context)
12
- @context = context
13
- @rules, @errors, @scope_stack = context.values_at("rules", "errors", "scope_stack")
11
+ def initialize(rules, scope_stack)
12
+ @rules = rules
13
+ @scope_stack = scope_stack
14
14
  end
15
15
 
16
- protected
17
-
18
- def run
16
+ def call
19
17
  return handle_invalid_args unless rules.is_a?(Array)
20
18
 
21
19
  rules.each_slice(2) do |condition_rule, value_rule|
22
20
  condition_result = evaluate(condition_rule)
23
- return condition_result if error?(condition_result)
24
21
  return condition_result if value_rule.nil?
25
22
 
26
23
  next unless Truthy.call(condition_result)
27
24
 
28
- value_result = evaluate(value_rule)
29
- return value_result if error?(value_result)
30
- return value_result
25
+ return evaluate(value_rule)
31
26
  end
32
27
 
33
28
  nil
@@ -36,14 +31,7 @@ module ShinyJsonLogic
36
31
  private
37
32
 
38
33
  def evaluate(rule)
39
- engine = Engine.new(rule, scope_stack: scope_stack)
40
- result = engine.call
41
- self.errors = [*errors, *engine.errors] if error?(result)
42
- result
43
- end
44
-
45
- def error?(result)
46
- result.is_a?(String) && result.match?(Try::SHINY_ERROR_PATTERN)
34
+ Engine.new(rule, scope_stack).call
47
35
  end
48
36
  end
49
37
  end
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Inclusion < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  needle = evaluate(rules.first)
10
8
  haystack = evaluate(rules.last)
11
9
  haystack.include?(needle)
@@ -4,7 +4,7 @@ module ShinyJsonLogic
4
4
  module Operations
5
5
  module Iterable
6
6
  class Base < Operations::Base
7
- def initialize(context)
7
+ def initialize(rules, scope_stack)
8
8
  super
9
9
 
10
10
  return handle_nil if dynamic_args?
@@ -20,39 +20,31 @@ module ShinyJsonLogic
20
20
  end
21
21
 
22
22
  def call
23
- return deliver if errors.any?
24
-
25
- deliver run
26
- end
27
-
28
- protected
29
-
30
- def run
31
23
  on_before
32
24
 
33
- collection.each_with_index.each_with_object([]) do |(item, index), results|
25
+ results = collection.each_with_index.each_with_object([]) do |(item, index), results|
34
26
  on_before_each(item, index)
35
- solved, solver = on_each(item)
36
- results << solved
37
- if solved.is_a?(String) && solved.match?(Try::SHINY_ERROR_PATTERN)
38
- # Propagate errors before breaking
39
- self.errors = [*self.errors, *solver.errors]
40
- # Clean up scopes pushed by on_before_each
27
+ begin
28
+ solved = on_each(item)
29
+ results << solved
30
+ on_after_each
31
+ rescue => e
32
+ # Clean up scopes before re-raising
41
33
  scope_stack.pop # item scope
42
34
  scope_stack.pop # iterator context scope
43
- break results
35
+ raise e
44
36
  end
45
- on_after_each(solved, solver)
46
- end.then do |results|
47
- on_after(results)
48
37
  end
38
+
39
+ on_after(results)
49
40
  end
50
41
 
42
+ protected
43
+
51
44
  private
52
45
 
53
46
  def on_each(_item)
54
- engine = Engine.new(filter, scope_stack: scope_stack)
55
- [engine.call, engine]
47
+ Engine.new(filter, scope_stack).call
56
48
  end
57
49
 
58
50
  def on_before_each(item, index = 0)
@@ -64,30 +56,22 @@ module ShinyJsonLogic
64
56
  scope_stack.push(item, index: index)
65
57
  end
66
58
 
67
- def on_after_each(_solved, solver)
59
+ def on_after_each
68
60
  # Pop the item scope
69
61
  scope_stack.pop
70
62
  # Pop the iterator context scope
71
63
  scope_stack.pop
72
- self.errors = [*self.errors, *solver.errors]
73
64
  end
74
65
 
75
66
  def on_before
76
67
  end
77
68
 
78
69
  def on_after(results)
79
- # If last result is an error, return it (not the array)
80
- if results.last.is_a?(String) && results.last.match?(Try::SHINY_ERROR_PATTERN)
81
- return results.last
82
- end
83
70
  results
84
71
  end
85
72
 
86
73
  def handle_nil
87
- error = Errors::Base.new(type: "Invalid Arguments")
88
- self.errors = [error]
89
-
90
- error.id
74
+ raise Errors::InvalidArguments
91
75
  end
92
76
 
93
77
  def setup_collection(collection)
@@ -1,18 +1,13 @@
1
1
  require "shiny_json_logic/operations/base"
2
+ require "shiny_json_logic/numericals/min_max_collection"
2
3
 
3
4
  module ShinyJsonLogic
4
5
  module Operations
5
6
  class Max < Base
6
- protected
7
+ include Numericals::MinMaxCollection
7
8
 
8
- def run
9
- result = nil
10
- Array.wrap_nil(rules).each do |rule|
11
- Array.wrap_nil(evaluate(rule)).each do |val|
12
- result = val if result.nil? || val > result
13
- end
14
- end
15
- result
9
+ def call
10
+ collect_numeric_values.max
16
11
  end
17
12
  end
18
13
  end
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Merge < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  Array.wrap_nil(rules).map do |rule|
10
8
  Array.wrap_nil(evaluate(rule))
11
9
  end.reduce([], :+)
@@ -1,18 +1,13 @@
1
1
  require "shiny_json_logic/operations/base"
2
+ require "shiny_json_logic/numericals/min_max_collection"
2
3
 
3
4
  module ShinyJsonLogic
4
5
  module Operations
5
6
  class Min < Base
6
- protected
7
+ include Numericals::MinMaxCollection
7
8
 
8
- def run
9
- result = nil
10
- rules.each do |rule|
11
- Array.wrap_nil(evaluate(rule)).each do |val|
12
- result = val if result.nil? || val < result
13
- end
14
- end
15
- result
9
+ def call
10
+ collect_numeric_values.min
16
11
  end
17
12
  end
18
13
  end
@@ -4,9 +4,7 @@ require "shiny_json_logic/operations/base"
4
4
  module ShinyJsonLogic
5
5
  module Operations
6
6
  class Missing < Base
7
- protected
8
-
9
- def run
7
+ def call
10
8
  items = Array.wrap_nil(rules)
11
9
  keys = []
12
10
  items.each do |rule|
@@ -4,16 +4,13 @@ require "shiny_json_logic/operations/missing"
4
4
  module ShinyJsonLogic
5
5
  module Operations
6
6
  class MissingSome < Missing
7
- protected
8
-
9
- def run
7
+ def call
10
8
  min_required = evaluate(rules[0])
11
9
  keys = Array.wrap_nil(evaluate(rules[1]))
12
10
  return keys unless data.is_a?(Hash) && rules.is_a?(Array)
13
11
 
14
12
  present = keys & data.keys
15
- ctx = { "rules" => keys, "errors" => errors, "scope_stack" => scope_stack }
16
- present.size >= min_required ? [] : Missing.new(ctx).call["result"]
13
+ present.size >= min_required ? [] : Missing.new(keys, scope_stack).call
17
14
  end
18
15
  end
19
16
  end
@@ -8,11 +8,9 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  include Numericals::Numerify
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  operands = Array.wrap_nil(rules)
15
- return handle_no_operators if operands.empty?
13
+ return handle_invalid_args if operands.empty?
16
14
 
17
15
  safe_arithmetic do
18
16
  result = nil
@@ -23,7 +21,7 @@ module ShinyJsonLogic
23
21
  result = result.nil? ? num : result.remainder(num)
24
22
  end
25
23
 
26
- return handle_no_operators if count < 2
24
+ return handle_invalid_args if count < 2
27
25
 
28
26
  result
29
27
  end
@@ -4,9 +4,7 @@ require "shiny_json_logic/truthy"
4
4
  module ShinyJsonLogic
5
5
  module Operations
6
6
  class Not < Base
7
- protected
8
-
9
- def run
7
+ def call
10
8
  value = rules.is_a?(Array) ? rules.first : rules
11
9
  !Truthy.call(evaluate(value))
12
10
  end
@@ -8,9 +8,7 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  raise_on_dynamic_args!
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  return handle_invalid_args if dynamic_args?
15
13
  return handle_invalid_args unless rules.is_a?(Array)
16
14
  return false if rules.empty?
@@ -3,16 +3,18 @@ require "shiny_json_logic/truthy"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Preserve < Iterable::Base
6
- def initialize(context)
7
- super
8
- @collection = context["rules"] || []
6
+ def initialize(rules, scope_stack)
7
+ @collection = Array.wrap(rules) || []
8
+ # Skip Iterable::Base initialization, go directly to Operations::Base
9
+ # Preserve doesn't need the standard iterable setup (filter, collection from rules[0], etc.)
10
+ @rules = rules
11
+ @scope_stack = scope_stack
9
12
  end
10
13
 
11
14
  private
12
15
 
13
16
  def on_each(item)
14
- engine = Engine.new(item, scope_stack: scope_stack)
15
- [engine.call, engine]
17
+ Engine.new(item, scope_stack).call
16
18
  end
17
19
 
18
20
  def on_after(results)
@@ -26,9 +28,8 @@ module ShinyJsonLogic
26
28
  # Don't push to scope stack
27
29
  end
28
30
 
29
- def on_after_each(_solved, solver)
31
+ def on_after_each
30
32
  # Don't pop from scope stack
31
- self.errors = [*self.errors, *solver.errors]
32
33
  end
33
34
  end
34
35
  end
@@ -8,9 +8,7 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  include Numericals::Numerify
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  operands = Array.wrap_nil(rules)
15
13
  return 1 if operands.empty?
16
14
 
@@ -8,12 +8,12 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  raise_on_dynamic_args!
10
10
 
11
- def initialize(context)
11
+ def initialize(rules, scope_stack)
12
+ # Capture initial accumulator before super (which may pre-process rules)
13
+ initial_accumulator_rule = rules.is_a?(Array) ? rules[2] : nil
12
14
  super
13
- return if errors.any?
14
-
15
15
  # Evaluate the initial accumulator value (third argument)
16
- @accumulator = Engine.new(context.dig("rules", 2), scope_stack: scope_stack).call
16
+ @accumulator = Engine.new(initial_accumulator_rule, scope_stack).call
17
17
  end
18
18
 
19
19
  private
@@ -31,9 +31,7 @@ module ShinyJsonLogic
31
31
  end
32
32
 
33
33
  def on_each(_item)
34
- engine = Engine.new(filter, scope_stack: scope_stack)
35
- self.accumulator = engine.call
36
- [self.accumulator, engine]
34
+ self.accumulator = Engine.new(filter, scope_stack).call
37
35
  end
38
36
 
39
37
  def on_after(_results)
@@ -9,9 +9,7 @@ module ShinyJsonLogic
9
9
  include Numericals::Numerify
10
10
  raise_on_dynamic_args!
11
11
 
12
- protected
13
-
14
- def run
12
+ def call
15
13
  return handle_invalid_args if dynamic_args?
16
14
  operands = Array.wrap_nil(rules)
17
15
  return handle_invalid_args if operands.length < 2
@@ -9,9 +9,7 @@ module ShinyJsonLogic
9
9
  include Numericals::Numerify
10
10
  raise_on_dynamic_args!
11
11
 
12
- protected
13
-
14
- def run
12
+ def call
15
13
  return handle_invalid_args if dynamic_args?
16
14
  operands = Array.wrap_nil(rules)
17
15
  return handle_invalid_args if operands.length < 2
@@ -7,9 +7,7 @@ module ShinyJsonLogic
7
7
  include Numericals::WithErrorHandling
8
8
  raise_on_dynamic_args!
9
9
 
10
- protected
11
-
12
- def run
10
+ def call
13
11
  return handle_invalid_args if dynamic_args?
14
12
  operands = Array.wrap_nil(rules)
15
13
  return handle_invalid_args if operands.length < 2
@@ -7,9 +7,7 @@ module ShinyJsonLogic
7
7
  include Numericals::WithErrorHandling
8
8
  raise_on_dynamic_args!
9
9
 
10
- protected
11
-
12
- def run
10
+ def call
13
11
  return handle_invalid_args if dynamic_args?
14
12
  operands = Array.wrap_nil(rules)
15
13
  return handle_invalid_args if operands.length < 2
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Substring < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  str = evaluate(rules[0]).to_s
10
8
  start = evaluate(rules[1]).to_i
11
9
  length = rules[2] ? evaluate(rules[2]).to_i : str.length
@@ -8,11 +8,9 @@ module ShinyJsonLogic
8
8
  include Numericals::WithErrorHandling
9
9
  include Numericals::Numerify
10
10
 
11
- protected
12
-
13
- def run
11
+ def call
14
12
  operands = Array.wrap_nil(rules)
15
- return handle_no_operators if operands.empty?
13
+ return handle_invalid_args if operands.empty?
16
14
 
17
15
  safe_arithmetic do
18
16
  result = nil
@@ -23,7 +21,7 @@ module ShinyJsonLogic
23
21
  result = result.nil? ? num : result - num
24
22
  end
25
23
 
26
- return handle_no_operators if count == 0
24
+ return handle_invalid_args if count == 0
27
25
  return result * -1 if count == 1
28
26
 
29
27
  result
@@ -3,7 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Throw < Base
6
- def run
6
+ def call
7
7
  raw_value = rules.is_a?(Array) ? rules[0] : rules
8
8
 
9
9
  error_type =
@@ -17,10 +17,9 @@ module ShinyJsonLogic
17
17
  extracted_type = self.data["type"] if extracted_type.nil?
18
18
  self.data["type"] = extracted_type unless extracted_type.nil?
19
19
 
20
- error = ShinyJsonLogic::Errors::Base.new(type: extracted_type)
21
- errors.push error
20
+ error = Errors::Base.new(type: extracted_type)
22
21
 
23
- error.id
22
+ raise error
24
23
  end
25
24
  end
26
25
  end
@@ -3,48 +3,43 @@ require "core_ext/array"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Try < Base
6
- SHINY_ERROR_PATTERN = /shiny_error_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
7
-
8
- protected
9
-
10
- def run
6
+ def call
11
7
  items = Array.wrap_nil(rules)
12
8
  last_error = nil
13
9
 
14
10
  items.each do |item|
15
11
  # If previous item was an error, switch context to error payload
16
- # Push two levels to match iterator convention:
17
- # - Level 1: empty context (like iterator's index context)
18
- # - Level 0: error payload (like iterator's current item)
19
12
  if last_error
20
13
  scope_stack.push({}) # intermediate level for [[1]] access
21
14
  scope_stack.push(last_error.payload)
22
15
  end
23
16
 
24
- engine = Engine.new(item, scope_stack: scope_stack)
25
- result = engine.call
17
+ begin
18
+ engine = Engine.new(item, scope_stack)
19
+ result = engine.call
26
20
 
27
- # Pop error contexts if we pushed them
28
- if last_error
29
- scope_stack.pop # error payload
30
- scope_stack.pop # intermediate level
31
- end
21
+ # Pop error contexts if we pushed them
22
+ if last_error
23
+ scope_stack.pop # error payload
24
+ scope_stack.pop # intermediate level
25
+ end
32
26
 
33
- # Check if result is an error
34
- if result.is_a?(String) && result.match?(SHINY_ERROR_PATTERN)
35
- # Find the error object
36
- last_error = engine.errors.find { |e| e.id == result }
37
- else
38
27
  # Found a valid result, return it
39
28
  return result
29
+ rescue ShinyJsonLogic::Errors::Base => e
30
+ # Pop error contexts if we pushed them
31
+ if last_error
32
+ scope_stack.pop # error payload
33
+ scope_stack.pop # intermediate level
34
+ end
35
+
36
+ last_error = e
37
+ # Don't add to errors array - we're handling it
40
38
  end
41
39
  end
42
40
 
43
41
  # All items were errors, re-raise the last one
44
- if last_error
45
- self.errors = [last_error]
46
- last_error.id
47
- end
42
+ raise last_error if last_error
48
43
  end
49
44
  end
50
45
  end
@@ -3,9 +3,7 @@ require "shiny_json_logic/operations/base"
3
3
  module ShinyJsonLogic
4
4
  module Operations
5
5
  class Val < Base
6
- protected
7
-
8
- def run
6
+ def call
9
7
  raw_keys = Array.wrap_nil(rules)
10
8
 
11
9
  # {"val": []} or {"val": null} - return current scope
@@ -4,9 +4,7 @@ require "shiny_json_logic/operations/base"
4
4
  module ShinyJsonLogic
5
5
  module Operations
6
6
  class Var < Base
7
- protected
8
-
9
- def run
7
+ def call
10
8
  items = Array.wrap_nil(rules)
11
9
  key = evaluate(items[0])
12
10
  default = items[1] ? evaluate(items[1]) : nil
@@ -1,3 +1,3 @@
1
1
  module ShinyJsonLogic
2
- VERSION = "0.2.8"
2
+ VERSION = "0.2.9"
3
3
  end
@@ -1,13 +1,43 @@
1
1
  require "shiny_json_logic/version"
2
2
  require "shiny_json_logic/engine"
3
+ require "shiny_json_logic/errors/base"
4
+ require "shiny_json_logic/errors/invalid_arguments"
5
+ require "shiny_json_logic/errors/not_a_number"
6
+ require "shiny_json_logic/errors/unknown_operator"
7
+ require "shiny_json_logic/operator_solver"
8
+ require "shiny_json_logic/scope_stack"
3
9
 
4
10
  module ShinyJsonLogic
5
11
  def self.apply(rule, data = {})
6
- engine = Engine.new(rule, data)
7
- engine.call.tap do |result|
8
- raise engine.errors.shift if engine.errors.any?
12
+ validate_operators!(rule)
13
+
14
+ scope_stack = ScopeStack.new(data || {})
15
+ engine = Engine.new(rule, scope_stack)
16
+ engine.call
17
+ end
9
18
 
10
- result
19
+ # Validates that all operations in the rule tree use known operators
20
+ def self.validate_operators!(rule)
21
+ case rule
22
+ when Hash
23
+ return if rule.empty?
24
+
25
+ # Multi-key hashes are invalid
26
+ if rule.size > 1
27
+ raise Errors::UnknownOperator
28
+ end
29
+
30
+ operation, args = rule.first
31
+
32
+ # Check if operation is known
33
+ unless OperatorSolver.new.solvers.key?(operation)
34
+ raise Errors::UnknownOperator
35
+ end
36
+
37
+ # Recursively validate args
38
+ validate_operators!(args)
39
+ when Array
40
+ rule.each { |item| validate_operators!(item) }
11
41
  end
12
42
  end
13
43
  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.2.8
4
+ version: 0.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Luis Moyano
@@ -131,6 +131,10 @@ files:
131
131
  - lib/shiny_json_logic.rb
132
132
  - lib/shiny_json_logic/engine.rb
133
133
  - lib/shiny_json_logic/errors/base.rb
134
+ - lib/shiny_json_logic/errors/invalid_arguments.rb
135
+ - lib/shiny_json_logic/errors/not_a_number.rb
136
+ - lib/shiny_json_logic/errors/unknown_operator.rb
137
+ - lib/shiny_json_logic/numericals/min_max_collection.rb
134
138
  - lib/shiny_json_logic/numericals/numerify.rb
135
139
  - lib/shiny_json_logic/numericals/with_error_handling.rb
136
140
  - lib/shiny_json_logic/operations/addition.rb