json-logic-rb 0.1.5 → 0.2.0
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/README.md +197 -197
- data/lib/json_logic/engine.rb +22 -21
- data/lib/json_logic/enumerable_operation.rb +27 -5
- data/lib/json_logic/errors/error.rb +29 -0
- data/lib/json_logic/errors/invalid_arguments_error.rb +7 -0
- data/lib/json_logic/errors/logic_error.rb +7 -0
- data/lib/json_logic/errors/nan_error.rb +7 -0
- data/lib/json_logic/ext/array.rb +5 -0
- data/lib/json_logic/operations/add.rb +3 -3
- data/lib/json_logic/operations/all.rb +3 -6
- data/lib/json_logic/operations/and.rb +6 -5
- data/lib/json_logic/operations/bool_cast.rb +2 -3
- data/lib/json_logic/operations/cat.rb +3 -1
- data/lib/json_logic/operations/coalesce.rb +9 -0
- data/lib/json_logic/operations/div.rb +7 -1
- data/lib/json_logic/operations/equal.rb +12 -3
- data/lib/json_logic/operations/exists.rb +14 -0
- data/lib/json_logic/operations/filter.rb +11 -3
- data/lib/json_logic/operations/gt.rb +12 -4
- data/lib/json_logic/operations/gte.rb +12 -4
- data/lib/json_logic/operations/if.rb +2 -0
- data/lib/json_logic/operations/in.rb +2 -0
- data/lib/json_logic/operations/lt.rb +10 -4
- data/lib/json_logic/operations/lte.rb +12 -4
- data/lib/json_logic/operations/map.rb +14 -2
- data/lib/json_logic/operations/max.rb +3 -1
- data/lib/json_logic/operations/merge.rb +4 -3
- data/lib/json_logic/operations/min.rb +3 -1
- data/lib/json_logic/operations/missing.rb +4 -26
- data/lib/json_logic/operations/missing_some.rb +6 -20
- data/lib/json_logic/operations/mod.rb +9 -1
- data/lib/json_logic/operations/mul.rb +4 -1
- data/lib/json_logic/operations/none.rb +4 -5
- data/lib/json_logic/operations/not_equal.rb +12 -3
- data/lib/json_logic/operations/or.rb +5 -1
- data/lib/json_logic/operations/preserve.rb +9 -0
- data/lib/json_logic/operations/reduce.rb +21 -5
- data/lib/json_logic/operations/some.rb +4 -7
- data/lib/json_logic/operations/strict_equal.rb +26 -3
- data/lib/json_logic/operations/strict_not_equal.rb +24 -3
- data/lib/json_logic/operations/sub.rb +8 -1
- data/lib/json_logic/operations/substr.rb +12 -20
- data/lib/json_logic/operations/ternary.rb +1 -7
- data/lib/json_logic/operations/throw.rb +12 -0
- data/lib/json_logic/operations/try.rb +35 -0
- data/lib/json_logic/operations/val.rb +79 -0
- data/lib/json_logic/operations/var.rb +22 -20
- data/lib/json_logic/scope.rb +67 -0
- data/lib/json_logic/semantics.rb +107 -38
- data/lib/json_logic/tree.rb +97 -0
- data/lib/json_logic/version.rb +1 -1
- data/lib/json_logic.rb +12 -0
- data/script/build_tests_json.rb +26 -0
- data/script/compliance.rb +160 -37
- data/spec/tmp/v2/tests.json +16981 -0
- metadata +24 -13
- /data/spec/tmp/{tests.json → v1/tests.json} +0 -0
|
@@ -2,10 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
using JsonLogic::Semantics
|
|
4
4
|
|
|
5
|
-
class JsonLogic::Operations::StrictNotEqual < JsonLogic::
|
|
5
|
+
class JsonLogic::Operations::StrictNotEqual < JsonLogic::LazyOperation
|
|
6
6
|
def self.name = "!=="
|
|
7
7
|
|
|
8
|
-
def call(
|
|
9
|
-
|
|
8
|
+
def call(args, data)
|
|
9
|
+
raise JsonLogic::InvalidArgumentsError.new unless args.is_a?(Array) && args.size >= 2
|
|
10
|
+
|
|
11
|
+
prev = JsonLogic.apply(args.first, data)
|
|
12
|
+
args[1..].each do |arg|
|
|
13
|
+
current = JsonLogic.apply(arg, data)
|
|
14
|
+
return false if strict_equal_value?(prev, current)
|
|
15
|
+
|
|
16
|
+
prev = current
|
|
17
|
+
end
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def strict_equal_value?(left, right)
|
|
24
|
+
if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
25
|
+
left.to_f == right.to_f
|
|
26
|
+
elsif left.is_a?(Array) || left.is_a?(Hash)
|
|
27
|
+
left.equal?(right)
|
|
28
|
+
else
|
|
29
|
+
left.class == right.class && left.eql?(right)
|
|
30
|
+
end
|
|
10
31
|
end
|
|
11
32
|
end
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::Sub < JsonLogic::Operation
|
|
4
6
|
def self.name = "-"
|
|
5
|
-
|
|
7
|
+
|
|
8
|
+
def call(args, _data)
|
|
9
|
+
raise JsonLogic::InvalidArgumentsError.new if args.empty?
|
|
10
|
+
numbers = args.map(&:to_f)
|
|
11
|
+
numbers.one? ? -numbers.first : numbers.drop(1).reduce(numbers.first, :-)
|
|
12
|
+
end
|
|
6
13
|
end
|
|
@@ -1,30 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::Substr < JsonLogic::Operation
|
|
4
6
|
def self.name = "substr"
|
|
5
7
|
|
|
6
|
-
def call(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
start
|
|
8
|
+
def call((string, index, length), _data)
|
|
9
|
+
value = string.to_s
|
|
10
|
+
start = index.to_i
|
|
11
|
+
start += value.length if start.negative?
|
|
12
|
+
start = start.clamp(0, value.length)
|
|
10
13
|
|
|
11
|
-
start
|
|
12
|
-
start = 0 if start < 0
|
|
13
|
-
start = str.length if start > str.length
|
|
14
|
+
return value[start..] || "" if length.nil?
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
size = length.to_i
|
|
17
|
+
return value.slice(start, size) || "" unless size.negative?
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
slice = str[start, l]
|
|
20
|
-
slice.nil? ? "" : slice
|
|
21
|
-
else
|
|
22
|
-
end_excl = str.length + l
|
|
23
|
-
end_excl = start if end_excl < start
|
|
24
|
-
end_excl = str.length if end_excl > str.length
|
|
25
|
-
length = end_excl - start
|
|
26
|
-
length = 0 if length < 0
|
|
27
|
-
str[start, length] || ""
|
|
28
|
-
end
|
|
19
|
+
finish = (value.length + size).clamp(start, value.length)
|
|
20
|
+
value.slice(start...finish) || ""
|
|
29
21
|
end
|
|
30
22
|
end
|
|
@@ -5,11 +5,5 @@ using JsonLogic::Semantics
|
|
|
5
5
|
class JsonLogic::Operations::Ternary < JsonLogic::LazyOperation
|
|
6
6
|
def self.name = "?:"
|
|
7
7
|
|
|
8
|
-
def call((cond_rule, then_rule, else_rule), data)
|
|
9
|
-
if !!JsonLogic.apply(cond_rule, data)
|
|
10
|
-
JsonLogic.apply(then_rule, data)
|
|
11
|
-
else
|
|
12
|
-
JsonLogic.apply(else_rule, data)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
8
|
+
def call((cond_rule, then_rule, else_rule), data) = !!JsonLogic.apply(cond_rule, data) ? JsonLogic.apply(then_rule, data) : JsonLogic.apply(else_rule, data)
|
|
15
9
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
5
|
+
class JsonLogic::Operations::Throw < JsonLogic::Operation
|
|
6
|
+
def self.name = "throw"
|
|
7
|
+
|
|
8
|
+
def call((value), _data)
|
|
9
|
+
type = value.is_a?(Hash) ? (value["type"] || value[:type]).to_s : value.to_s
|
|
10
|
+
raise JsonLogic::Error, type
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
5
|
+
class JsonLogic::Operations::Try < JsonLogic::LazyOperation
|
|
6
|
+
def self.name = "try"
|
|
7
|
+
|
|
8
|
+
def call(arguments, data)
|
|
9
|
+
arguments = Array.wrap(arguments)
|
|
10
|
+
raise JsonLogic::InvalidArgumentsError.new if arguments.empty?
|
|
11
|
+
|
|
12
|
+
state = { data: data, error: nil }
|
|
13
|
+
|
|
14
|
+
arguments.each do |expression|
|
|
15
|
+
begin
|
|
16
|
+
value = JsonLogic.apply(expression, state[:data])
|
|
17
|
+
return value unless value.is_a?(Float) && value.nan?
|
|
18
|
+
|
|
19
|
+
state[:error] = JsonLogic::NaNError.new
|
|
20
|
+
rescue JsonLogic::Error => error
|
|
21
|
+
state[:error] = error
|
|
22
|
+
rescue ArgumentError, IndexError, TypeError, NoMethodError
|
|
23
|
+
state[:error] = JsonLogic::InvalidArgumentsError.new
|
|
24
|
+
rescue ZeroDivisionError, FloatDomainError
|
|
25
|
+
state[:error] = JsonLogic::NaNError.new
|
|
26
|
+
rescue StandardError => error
|
|
27
|
+
state[:error] = JsonLogic::Error.new(error.message)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
state[:data] = state[:error].payload
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
raise state[:error]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
5
|
+
class JsonLogic::Operations::Val < JsonLogic::Operation
|
|
6
|
+
def self.name = "val"
|
|
7
|
+
def self.values_only? = false
|
|
8
|
+
|
|
9
|
+
def call(args, data)
|
|
10
|
+
path = JsonLogic.apply(path_rule(args), data)
|
|
11
|
+
return root_value(path, data) if root_path?(path)
|
|
12
|
+
|
|
13
|
+
base, segments = resolve_base_and_segments(path, data)
|
|
14
|
+
JsonLogic::Tree.new(base).dig(segments, split_dots: false)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def path_rule(args) = args.is_a?(Array) && args.one? ? args.first : args
|
|
20
|
+
|
|
21
|
+
def root_path?(path)
|
|
22
|
+
path.nil? || (path.is_a?(Array) && path.empty?) || (path.is_a?(String) && path.empty?)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def root_value(path, data)
|
|
26
|
+
return data if path.nil?
|
|
27
|
+
return scoped_root_value(data) if path.is_a?(Array) && path.empty?
|
|
28
|
+
return data[""] if data.is_a?(Hash) && data.key?("")
|
|
29
|
+
|
|
30
|
+
data
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def scoped_root_value(data)
|
|
34
|
+
return data[""] if data.is_a?(Hash) && data["__scope__"] && data.key?("")
|
|
35
|
+
|
|
36
|
+
data
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def resolve_base_and_segments(path, data)
|
|
40
|
+
segments = path.is_a?(Array) ? path : [path]
|
|
41
|
+
return [data, segments] unless segments.first.is_a?(Array)
|
|
42
|
+
|
|
43
|
+
hop = segments.first.first.to_i
|
|
44
|
+
[base_for_hop(data, hop), segments.drop(1)]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def base_for_hop(base, hop)
|
|
48
|
+
return base if hop.zero?
|
|
49
|
+
return base_for_positive_hop(base, hop) if hop.positive?
|
|
50
|
+
|
|
51
|
+
(-hop - 1).times do
|
|
52
|
+
parent = parent_of(base)
|
|
53
|
+
break unless parent
|
|
54
|
+
|
|
55
|
+
base = parent
|
|
56
|
+
end
|
|
57
|
+
base
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def base_for_positive_hop(base, hop)
|
|
61
|
+
moved = false
|
|
62
|
+
(hop - 1).times do
|
|
63
|
+
parent = parent_of(base)
|
|
64
|
+
break unless parent
|
|
65
|
+
|
|
66
|
+
base = parent
|
|
67
|
+
moved = true
|
|
68
|
+
end
|
|
69
|
+
return base if moved
|
|
70
|
+
|
|
71
|
+
stack = Thread.current[:json_logic_scope_stack] || []
|
|
72
|
+
idx = stack.length - hop
|
|
73
|
+
return base unless idx >= 0 && idx < stack.length
|
|
74
|
+
|
|
75
|
+
stack[idx]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parent_of(value) = value.is_a?(Hash) ? value["__parent__"] : nil
|
|
79
|
+
end
|
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
using JsonLogic::Semantics
|
|
4
|
+
|
|
3
5
|
class JsonLogic::Operations::Var < JsonLogic::Operation
|
|
4
|
-
def self.name = "var"
|
|
6
|
+
def self.name = "var"
|
|
5
7
|
def self.values_only? = false
|
|
6
8
|
|
|
7
|
-
def call(
|
|
9
|
+
def call(args, data)
|
|
10
|
+
json = JsonLogic::Tree.new(data)
|
|
11
|
+
|
|
12
|
+
if args.is_a?(Array)
|
|
13
|
+
path_rule = args[0]
|
|
14
|
+
fallback_rule = args[1]
|
|
15
|
+
else
|
|
16
|
+
path_rule = args
|
|
17
|
+
fallback_rule = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
path = JsonLogic.apply(path_rule, data)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
if path.is_a?(String) && path.empty?
|
|
22
|
+
return data[""] if data.is_a?(Hash) && data.key?("")
|
|
23
|
+
return data
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return json.dig(path, split_dots: true) if json.exists?(path, split_dots: true)
|
|
27
|
+
|
|
12
28
|
return nil if fallback_rule.nil?
|
|
13
|
-
JsonLogic.apply(fallback_rule, data)
|
|
14
|
-
end
|
|
15
29
|
|
|
16
|
-
|
|
17
|
-
return nil if obj.nil?
|
|
18
|
-
cur = obj
|
|
19
|
-
path.to_s.split(".").each do |k|
|
|
20
|
-
if cur.is_a?(Array) && k =~ /\A\d+\z/
|
|
21
|
-
cur = cur[k.to_i]
|
|
22
|
-
elsif cur.is_a?(Hash)
|
|
23
|
-
cur = cur[k] || cur[k.to_s] || cur[k.to_sym]
|
|
24
|
-
else
|
|
25
|
-
return nil
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
cur
|
|
30
|
+
JsonLogic.apply(fallback_rule, data)
|
|
29
31
|
end
|
|
30
32
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsonLogic
|
|
4
|
+
# Per-item evaluation scope for enumerable operations.
|
|
5
|
+
# Keeps meta-fields virtual so they are not written into item hashes.
|
|
6
|
+
class Scope < Hash
|
|
7
|
+
ROOT_KEY = ""
|
|
8
|
+
VALUE_KEY = "value"
|
|
9
|
+
INDEX_KEY = "index"
|
|
10
|
+
PARENT_KEY = "__parent__"
|
|
11
|
+
SCOPE_KEY = "__scope__"
|
|
12
|
+
|
|
13
|
+
def initialize(item, parent, index)
|
|
14
|
+
super()
|
|
15
|
+
update(item) if item.is_a?(Hash)
|
|
16
|
+
@item = item
|
|
17
|
+
@parent = parent
|
|
18
|
+
@index = index
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def [](key)
|
|
22
|
+
normalized = normalize_key(key)
|
|
23
|
+
return @parent if normalized == PARENT_KEY
|
|
24
|
+
return true if normalized == SCOPE_KEY
|
|
25
|
+
|
|
26
|
+
if super_key?(normalized)
|
|
27
|
+
super(normalized)
|
|
28
|
+
else
|
|
29
|
+
virtual_value(normalized)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def key?(key)
|
|
34
|
+
normalized = normalize_key(key)
|
|
35
|
+
return true if normalized == PARENT_KEY || normalized == SCOPE_KEY
|
|
36
|
+
return true if normalized == ROOT_KEY || normalized == VALUE_KEY || normalized == INDEX_KEY
|
|
37
|
+
return true if super_key?(normalized)
|
|
38
|
+
|
|
39
|
+
!virtual_value(normalized).nil?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def normalize_key(key)
|
|
45
|
+
case key
|
|
46
|
+
when Symbol then key.to_s
|
|
47
|
+
else key
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def super_key?(key)
|
|
52
|
+
hash_has_key?(key) || (key.is_a?(String) && hash_has_key?(key.to_sym))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def hash_has_key?(key)
|
|
56
|
+
Hash.instance_method(:key?).bind_call(self, key)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def virtual_value(key)
|
|
60
|
+
return @item if key == ROOT_KEY
|
|
61
|
+
return @item if key == VALUE_KEY
|
|
62
|
+
return @index if key == INDEX_KEY
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/json_logic/semantics.rb
CHANGED
|
@@ -34,7 +34,7 @@ module JsonLogic
|
|
|
34
34
|
when TrueClass then 1.0
|
|
35
35
|
when FalseClass then 0.0
|
|
36
36
|
when NilClass then 0.0
|
|
37
|
-
when Array then
|
|
37
|
+
when Array then Float::NAN
|
|
38
38
|
when String
|
|
39
39
|
s = v.strip
|
|
40
40
|
return 0.0 if s.empty?
|
|
@@ -49,41 +49,14 @@ module JsonLogic
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def eq(a, b)
|
|
52
|
-
if a.
|
|
53
|
-
|
|
54
|
-
ax = a.to_f; bx = b.to_f
|
|
55
|
-
return false if ax.nan? || bx.nan?
|
|
56
|
-
return ax == bx
|
|
57
|
-
else
|
|
58
|
-
return a.eql?(b)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
if a.nil? && b.nil?
|
|
63
|
-
return true
|
|
64
|
-
elsif a.nil? || b.nil?
|
|
65
|
-
return false
|
|
66
|
-
end
|
|
52
|
+
return a.eql?(b) if a.is_a?(String) && b.is_a?(String)
|
|
53
|
+
raise FloatDomainError, "NaN" if a.is_a?(Array) || b.is_a?(Array) || a.is_a?(Hash) || b.is_a?(Hash)
|
|
67
54
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if b.is_a?(TrueClass) || b.is_a?(FalseClass)
|
|
72
|
-
return eq(a, num(b))
|
|
73
|
-
end
|
|
55
|
+
ax = num(a)
|
|
56
|
+
bx = num(b)
|
|
57
|
+
raise FloatDomainError, "NaN" if ax.nan? || bx.nan?
|
|
74
58
|
|
|
75
|
-
|
|
76
|
-
ax = num(a); bx = num(b)
|
|
77
|
-
return false if ax.nan? || bx.nan?
|
|
78
|
-
return ax == bx
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
if (a.is_a?(Array) && (b.is_a?(String) || b.is_a?(Numeric))) ||
|
|
82
|
-
(b.is_a?(Array) && (a.is_a?(String) || a.is_a?(Numeric)))
|
|
83
|
-
return eq(to_primitive(a), to_primitive(b))
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
false
|
|
59
|
+
ax == bx
|
|
87
60
|
end
|
|
88
61
|
|
|
89
62
|
def cmp(a, b)
|
|
@@ -96,6 +69,13 @@ module JsonLogic
|
|
|
96
69
|
end
|
|
97
70
|
end
|
|
98
71
|
|
|
72
|
+
def cmp!(a, b)
|
|
73
|
+
c = cmp(a, b)
|
|
74
|
+
raise FloatDomainError, "NaN" if c.nil?
|
|
75
|
+
|
|
76
|
+
c
|
|
77
|
+
end
|
|
78
|
+
|
|
99
79
|
refine Object do
|
|
100
80
|
def !@
|
|
101
81
|
JsonLogic::Semantics.truthy?(self) ? false : true
|
|
@@ -108,11 +88,100 @@ module JsonLogic
|
|
|
108
88
|
|
|
109
89
|
[String, Integer, Float, NilClass, Array, TrueClass, FalseClass].each do |klass|
|
|
110
90
|
refine klass do
|
|
91
|
+
def to_f
|
|
92
|
+
case self
|
|
93
|
+
when Integer, Float
|
|
94
|
+
self * 1.0
|
|
95
|
+
when TrueClass
|
|
96
|
+
1.0
|
|
97
|
+
when FalseClass, NilClass
|
|
98
|
+
0.0
|
|
99
|
+
when String
|
|
100
|
+
s = strip
|
|
101
|
+
return 0.0 if s.empty?
|
|
102
|
+
Float(s)
|
|
103
|
+
when Array
|
|
104
|
+
raise FloatDomainError, "NaN"
|
|
105
|
+
else
|
|
106
|
+
raise FloatDomainError, "NaN"
|
|
107
|
+
end
|
|
108
|
+
rescue ArgumentError
|
|
109
|
+
raise FloatDomainError, "NaN"
|
|
110
|
+
end
|
|
111
|
+
|
|
111
112
|
def ==(other) = JsonLogic::Semantics.eq(self, other)
|
|
112
|
-
def >(other)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
def >(other)
|
|
114
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
115
|
+
when 1 then true
|
|
116
|
+
else false
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def >=(other)
|
|
121
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
122
|
+
when 1, 0 then true
|
|
123
|
+
else false
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def <(other)
|
|
128
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
129
|
+
when -1 then true
|
|
130
|
+
else false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def <=(other)
|
|
135
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
136
|
+
when -1, 0 then true
|
|
137
|
+
else false
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
refine Hash do
|
|
144
|
+
def to_f
|
|
145
|
+
raise FloatDomainError, "NaN"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ==(other)
|
|
149
|
+
return eql?(other) if other.is_a?(Hash)
|
|
150
|
+
JsonLogic::Semantics.eq(self, other)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def >(other)
|
|
154
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
155
|
+
when 1 then true
|
|
156
|
+
else false
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def >=(other)
|
|
161
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
162
|
+
when 1, 0 then true
|
|
163
|
+
else false
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def <(other)
|
|
168
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
169
|
+
when -1 then true
|
|
170
|
+
else false
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def <=(other)
|
|
175
|
+
case JsonLogic::Semantics.cmp!(self, other)
|
|
176
|
+
when -1, 0 then true
|
|
177
|
+
else false
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
refine Object do
|
|
183
|
+
def to_f
|
|
184
|
+
raise FloatDomainError, "NaN"
|
|
116
185
|
end
|
|
117
186
|
end
|
|
118
187
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JsonLogic
|
|
4
|
+
class Tree
|
|
5
|
+
INDEX_PATTERN = /\A-?\d+\z/
|
|
6
|
+
|
|
7
|
+
def initialize(data)
|
|
8
|
+
@data = data
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dig(path, split_dots: true)
|
|
12
|
+
_found, value = lookup(path, split_dots: split_dots)
|
|
13
|
+
value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def exists?(path, split_dots: true)
|
|
17
|
+
lookup(path, split_dots: split_dots).first
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def lookup(path, split_dots: true)
|
|
23
|
+
return [true, @data] if root_path?(path)
|
|
24
|
+
|
|
25
|
+
current = @data
|
|
26
|
+
each_segment(path, split_dots: split_dots).each do |segment|
|
|
27
|
+
found, current = step(current, segment)
|
|
28
|
+
return [false, nil] unless found
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
[true, current]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def root_path?(path)
|
|
35
|
+
path.nil? || (path.is_a?(String) && path.empty?) || (path.is_a?(Array) && path.empty?)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def step(current, segment)
|
|
39
|
+
case current
|
|
40
|
+
when Hash
|
|
41
|
+
fetch_from_hash(current, segment)
|
|
42
|
+
when Array
|
|
43
|
+
fetch_from_array(current, segment)
|
|
44
|
+
else
|
|
45
|
+
[false, nil]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fetch_from_hash(current, segment)
|
|
50
|
+
key = find_hash_key(current, segment)
|
|
51
|
+
return [false, nil] if key.nil?
|
|
52
|
+
|
|
53
|
+
[true, current[key]]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def find_hash_key(current, segment)
|
|
57
|
+
keys = [segment]
|
|
58
|
+
keys << segment.to_sym if segment.is_a?(String)
|
|
59
|
+
keys << segment.to_s
|
|
60
|
+
keys.find { |key| current.key?(key) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def fetch_from_array(current, segment)
|
|
64
|
+
index = normalize_index(segment, current.length)
|
|
65
|
+
return [false, nil] if index.nil?
|
|
66
|
+
|
|
67
|
+
[true, current[index]]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def each_segment(path, split_dots: true)
|
|
71
|
+
case path
|
|
72
|
+
when Array
|
|
73
|
+
path.flat_map { |segment| split_segment(segment, split_dots: split_dots) }
|
|
74
|
+
else
|
|
75
|
+
split_segment(path, split_dots: split_dots)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def split_segment(segment, split_dots: true)
|
|
80
|
+
if split_dots && segment.is_a?(String) && !segment.empty? && !segment.eql?(".")
|
|
81
|
+
segment.split('.')
|
|
82
|
+
else
|
|
83
|
+
[segment]
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def normalize_index(segment, size)
|
|
88
|
+
return nil unless segment.to_s.match?(INDEX_PATTERN)
|
|
89
|
+
|
|
90
|
+
idx = segment.to_i
|
|
91
|
+
idx += size if idx.negative?
|
|
92
|
+
return nil if idx.negative? || idx >= size
|
|
93
|
+
|
|
94
|
+
idx
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
data/lib/json_logic/version.rb
CHANGED
data/lib/json_logic.rb
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'json_logic/version'
|
|
4
4
|
require_relative 'json_logic/semantics'
|
|
5
|
+
require_relative 'json_logic/errors/error'
|
|
6
|
+
require_relative 'json_logic/errors/logic_error'
|
|
7
|
+
require_relative 'json_logic/errors/invalid_arguments_error'
|
|
8
|
+
require_relative 'json_logic/errors/nan_error'
|
|
9
|
+
require_relative 'json_logic/ext/array'
|
|
10
|
+
require_relative 'json_logic/tree'
|
|
11
|
+
require_relative 'json_logic/scope'
|
|
5
12
|
require_relative 'json_logic/operation'
|
|
6
13
|
require_relative 'json_logic/lazy_operation'
|
|
7
14
|
require_relative 'json_logic/enumerable_operation'
|
|
@@ -34,7 +41,12 @@ module JsonLogic
|
|
|
34
41
|
|
|
35
42
|
class << self
|
|
36
43
|
def apply(rule, data = nil)
|
|
44
|
+
stack = (Thread.current[:json_logic_scope_stack] ||= [])
|
|
45
|
+
stack << data
|
|
37
46
|
Engine.default.evaluate(rule, data)
|
|
47
|
+
ensure
|
|
48
|
+
stack.pop
|
|
49
|
+
Thread.current[:json_logic_scope_stack] = nil if stack.empty?
|
|
38
50
|
end
|
|
39
51
|
|
|
40
52
|
def add_operation(name, lazy: false, &block)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
source_dir = ARGV[0] || '/tmp/compat-tables/suites'
|
|
7
|
+
out_file = ARGV[1] || File.expand_path('../spec/tmp/tests.json', __dir__)
|
|
8
|
+
|
|
9
|
+
index_file = File.join(source_dir, 'index.json')
|
|
10
|
+
abort("index.json not found at #{index_file}") unless File.exist?(index_file)
|
|
11
|
+
|
|
12
|
+
index = JSON.parse(File.read(index_file))
|
|
13
|
+
merged = []
|
|
14
|
+
|
|
15
|
+
index.each do |relative_path|
|
|
16
|
+
suite_file = File.join(source_dir, relative_path)
|
|
17
|
+
abort("suite file not found: #{suite_file}") unless File.exist?(suite_file)
|
|
18
|
+
|
|
19
|
+
merged << "# suite: #{relative_path}"
|
|
20
|
+
suite_entries = JSON.parse(File.read(suite_file))
|
|
21
|
+
suite_entries.each { |entry| merged << entry }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
File.write(out_file, JSON.pretty_generate(merged) + "\n")
|
|
25
|
+
case_count = merged.count { |entry| entry.is_a?(Hash) && entry.key?('rule') }
|
|
26
|
+
puts "Wrote #{out_file} with #{case_count} rule cases from #{index.size} suites"
|