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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile.lock +1 -1
- data/bin/test.sh +57 -0
- data/lib/shiny_json_logic/engine.rb +5 -25
- data/lib/shiny_json_logic/errors/base.rb +1 -5
- data/lib/shiny_json_logic/errors/invalid_arguments.rb +11 -0
- data/lib/shiny_json_logic/errors/not_a_number.rb +11 -0
- data/lib/shiny_json_logic/errors/unknown_operator.rb +11 -0
- data/lib/shiny_json_logic/numericals/min_max_collection.rb +31 -0
- data/lib/shiny_json_logic/numericals/with_error_handling.rb +4 -10
- data/lib/shiny_json_logic/operations/addition.rb +1 -3
- data/lib/shiny_json_logic/operations/and.rb +1 -3
- data/lib/shiny_json_logic/operations/base.rb +12 -25
- data/lib/shiny_json_logic/operations/coalesce.rb +1 -3
- data/lib/shiny_json_logic/operations/concatenation.rb +1 -3
- data/lib/shiny_json_logic/operations/different.rb +1 -3
- data/lib/shiny_json_logic/operations/division.rb +1 -3
- data/lib/shiny_json_logic/operations/double_not.rb +1 -3
- data/lib/shiny_json_logic/operations/equal.rb +1 -3
- data/lib/shiny_json_logic/operations/exists.rb +1 -3
- data/lib/shiny_json_logic/operations/filter.rb +1 -2
- data/lib/shiny_json_logic/operations/greater.rb +1 -3
- data/lib/shiny_json_logic/operations/greater_equal.rb +1 -3
- data/lib/shiny_json_logic/operations/if.rb +6 -18
- data/lib/shiny_json_logic/operations/inclusion.rb +1 -3
- data/lib/shiny_json_logic/operations/iterable/base.rb +16 -32
- data/lib/shiny_json_logic/operations/max.rb +4 -9
- data/lib/shiny_json_logic/operations/merge.rb +1 -3
- data/lib/shiny_json_logic/operations/min.rb +4 -9
- data/lib/shiny_json_logic/operations/missing.rb +1 -3
- data/lib/shiny_json_logic/operations/missing_some.rb +2 -5
- data/lib/shiny_json_logic/operations/modulo.rb +3 -5
- data/lib/shiny_json_logic/operations/not.rb +1 -3
- data/lib/shiny_json_logic/operations/or.rb +1 -3
- data/lib/shiny_json_logic/operations/preserve.rb +8 -7
- data/lib/shiny_json_logic/operations/product.rb +1 -3
- data/lib/shiny_json_logic/operations/reduce.rb +5 -7
- data/lib/shiny_json_logic/operations/smaller.rb +1 -3
- data/lib/shiny_json_logic/operations/smaller_equal.rb +1 -3
- data/lib/shiny_json_logic/operations/strict_different.rb +1 -3
- data/lib/shiny_json_logic/operations/strict_equal.rb +1 -3
- data/lib/shiny_json_logic/operations/substring.rb +1 -3
- data/lib/shiny_json_logic/operations/subtraction.rb +3 -5
- data/lib/shiny_json_logic/operations/throw.rb +3 -4
- data/lib/shiny_json_logic/operations/try.rb +19 -24
- data/lib/shiny_json_logic/operations/val.rb +1 -3
- data/lib/shiny_json_logic/operations/var.rb +1 -3
- data/lib/shiny_json_logic/version.rb +1 -1
- data/lib/shiny_json_logic.rb +34 -4
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecf7aea99599023d6adf2af12bbb99fe8a40d9d8bcce1c2410eb4a49337b08b6
|
|
4
|
+
data.tar.gz: 73944111a179b14d04e401351519c46400294b39e95571b3f744225915f759a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
-
|
|
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
|
-
@
|
|
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.
|
|
17
|
+
return rule unless operations.key?(operation)
|
|
25
18
|
|
|
26
|
-
|
|
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
|
|
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,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
|
-
|
|
18
|
-
self.errors << error
|
|
19
|
-
error.id
|
|
19
|
+
raise Errors::NotANumber
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def handle_invalid_args
|
|
23
|
-
|
|
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
|
raise_on_dynamic_args!
|
|
10
10
|
|
|
11
|
-
|
|
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(
|
|
7
|
-
@
|
|
8
|
-
@
|
|
9
|
-
@
|
|
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
|
-
|
|
13
|
+
raise NotImplementedError
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
protected
|
|
18
17
|
|
|
19
|
-
attr_reader :
|
|
20
|
-
attr_accessor :rules
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
|
@@ -10,8 +10,7 @@ module ShinyJsonLogic
|
|
|
10
10
|
private
|
|
11
11
|
|
|
12
12
|
def on_each(item)
|
|
13
|
-
|
|
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)
|
|
@@ -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(
|
|
12
|
-
@
|
|
13
|
-
@
|
|
11
|
+
def initialize(rules, scope_stack)
|
|
12
|
+
@rules = rules
|
|
13
|
+
@scope_stack = scope_stack
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -4,7 +4,7 @@ module ShinyJsonLogic
|
|
|
4
4
|
module Operations
|
|
5
5
|
module Iterable
|
|
6
6
|
class Base < Operations::Base
|
|
7
|
-
def initialize(
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Clean up scopes
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
include Numericals::MinMaxCollection
|
|
7
8
|
|
|
8
|
-
def
|
|
9
|
-
|
|
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
|
|
@@ -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
|
-
|
|
7
|
+
include Numericals::MinMaxCollection
|
|
7
8
|
|
|
8
|
-
def
|
|
9
|
-
|
|
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,16 +4,13 @@ require "shiny_json_logic/operations/missing"
|
|
|
4
4
|
module ShinyJsonLogic
|
|
5
5
|
module Operations
|
|
6
6
|
class MissingSome < Missing
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
def run
|
|
11
|
+
def call
|
|
14
12
|
operands = Array.wrap_nil(rules)
|
|
15
|
-
return
|
|
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
|
|
24
|
+
return handle_invalid_args if count < 2
|
|
27
25
|
|
|
28
26
|
result
|
|
29
27
|
end
|
|
@@ -8,9 +8,7 @@ module ShinyJsonLogic
|
|
|
8
8
|
include Numericals::WithErrorHandling
|
|
9
9
|
raise_on_dynamic_args!
|
|
10
10
|
|
|
11
|
-
|
|
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(
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
|
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,12 +8,12 @@ module ShinyJsonLogic
|
|
|
8
8
|
include Numericals::WithErrorHandling
|
|
9
9
|
raise_on_dynamic_args!
|
|
10
10
|
|
|
11
|
-
def initialize(
|
|
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(
|
|
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
|
-
|
|
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)
|
|
@@ -7,9 +7,7 @@ module ShinyJsonLogic
|
|
|
7
7
|
include Numericals::WithErrorHandling
|
|
8
8
|
raise_on_dynamic_args!
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
def run
|
|
11
|
+
def call
|
|
14
12
|
operands = Array.wrap_nil(rules)
|
|
15
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
21
|
-
errors.push error
|
|
20
|
+
error = Errors::Base.new(type: extracted_type)
|
|
22
21
|
|
|
23
|
-
error
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
17
|
+
begin
|
|
18
|
+
engine = Engine.new(item, scope_stack)
|
|
19
|
+
result = engine.call
|
|
26
20
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
data/lib/shiny_json_logic.rb
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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.
|
|
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
|