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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +197 -197
  3. data/lib/json_logic/engine.rb +22 -21
  4. data/lib/json_logic/enumerable_operation.rb +27 -5
  5. data/lib/json_logic/errors/error.rb +29 -0
  6. data/lib/json_logic/errors/invalid_arguments_error.rb +7 -0
  7. data/lib/json_logic/errors/logic_error.rb +7 -0
  8. data/lib/json_logic/errors/nan_error.rb +7 -0
  9. data/lib/json_logic/ext/array.rb +5 -0
  10. data/lib/json_logic/operations/add.rb +3 -3
  11. data/lib/json_logic/operations/all.rb +3 -6
  12. data/lib/json_logic/operations/and.rb +6 -5
  13. data/lib/json_logic/operations/bool_cast.rb +2 -3
  14. data/lib/json_logic/operations/cat.rb +3 -1
  15. data/lib/json_logic/operations/coalesce.rb +9 -0
  16. data/lib/json_logic/operations/div.rb +7 -1
  17. data/lib/json_logic/operations/equal.rb +12 -3
  18. data/lib/json_logic/operations/exists.rb +14 -0
  19. data/lib/json_logic/operations/filter.rb +11 -3
  20. data/lib/json_logic/operations/gt.rb +12 -4
  21. data/lib/json_logic/operations/gte.rb +12 -4
  22. data/lib/json_logic/operations/if.rb +2 -0
  23. data/lib/json_logic/operations/in.rb +2 -0
  24. data/lib/json_logic/operations/lt.rb +10 -4
  25. data/lib/json_logic/operations/lte.rb +12 -4
  26. data/lib/json_logic/operations/map.rb +14 -2
  27. data/lib/json_logic/operations/max.rb +3 -1
  28. data/lib/json_logic/operations/merge.rb +4 -3
  29. data/lib/json_logic/operations/min.rb +3 -1
  30. data/lib/json_logic/operations/missing.rb +4 -26
  31. data/lib/json_logic/operations/missing_some.rb +6 -20
  32. data/lib/json_logic/operations/mod.rb +9 -1
  33. data/lib/json_logic/operations/mul.rb +4 -1
  34. data/lib/json_logic/operations/none.rb +4 -5
  35. data/lib/json_logic/operations/not_equal.rb +12 -3
  36. data/lib/json_logic/operations/or.rb +5 -1
  37. data/lib/json_logic/operations/preserve.rb +9 -0
  38. data/lib/json_logic/operations/reduce.rb +21 -5
  39. data/lib/json_logic/operations/some.rb +4 -7
  40. data/lib/json_logic/operations/strict_equal.rb +26 -3
  41. data/lib/json_logic/operations/strict_not_equal.rb +24 -3
  42. data/lib/json_logic/operations/sub.rb +8 -1
  43. data/lib/json_logic/operations/substr.rb +12 -20
  44. data/lib/json_logic/operations/ternary.rb +1 -7
  45. data/lib/json_logic/operations/throw.rb +12 -0
  46. data/lib/json_logic/operations/try.rb +35 -0
  47. data/lib/json_logic/operations/val.rb +79 -0
  48. data/lib/json_logic/operations/var.rb +22 -20
  49. data/lib/json_logic/scope.rb +67 -0
  50. data/lib/json_logic/semantics.rb +107 -38
  51. data/lib/json_logic/tree.rb +97 -0
  52. data/lib/json_logic/version.rb +1 -1
  53. data/lib/json_logic.rb +12 -0
  54. data/script/build_tests_json.rb +26 -0
  55. data/script/compliance.rb +160 -37
  56. data/spec/tmp/v2/tests.json +16981 -0
  57. metadata +24 -13
  58. /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::Operation
5
+ class JsonLogic::Operations::StrictNotEqual < JsonLogic::LazyOperation
6
6
  def self.name = "!=="
7
7
 
8
- def call((a,b), _data)
9
- !(a === b)
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
- def call(values, _data) = (values.size == 1 ? -values[0].to_f : values[0].to_f - values[1].to_f)
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(values, _data)
7
- s, i, len = values
8
- str = s.to_s
9
- start = i.to_i
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 += str.length if start < 0
12
- start = 0 if start < 0
13
- start = str.length if start > str.length
14
+ return value[start..] || "" if length.nil?
14
15
 
15
- return (str[start..-1] || "") if len.nil?
16
+ size = length.to_i
17
+ return value.slice(start, size) || "" unless size.negative?
16
18
 
17
- l = len.to_i
18
- if l >= 0
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((path_rule, fallback_rule), data)
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
- return data if path == ""
10
- val = dig(data, path)
11
- return val unless val.nil?
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
- def dig(obj, path)
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
@@ -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 num(to_primitive(v))
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.class == b.class
53
- if a.is_a?(Numeric)
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
- if a.is_a?(TrueClass) || a.is_a?(FalseClass)
69
- return eq(num(a), b)
70
- end
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
- if (a.is_a?(String) && b.is_a?(Numeric)) || (a.is_a?(Numeric) && b.is_a?(String))
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) = (c = JsonLogic::Semantics.cmp(self, other)) && c == 1
113
- def >=(other) = (c = JsonLogic::Semantics.cmp(self, other)) && (c == 1 || c == 0)
114
- def <(other) = (c = JsonLogic::Semantics.cmp(self, other)) && c == -1
115
- def <=(other) = (c = JsonLogic::Semantics.cmp(self, other)) && (c == -1 || c == 0)
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
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module JsonLogic; VERSION = '0.1.5'; end
3
+ module JsonLogic; VERSION = '0.2.0'; end
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"