Skip to content

Commit b30270c

Browse files
authored
fix: Add support of complex not (!) operator (#18)
1 parent 9901afa commit b30270c

File tree

6 files changed

+147
-28
lines changed

6 files changed

+147
-28
lines changed

Gemfile.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
json_logic_ruby (0.2.3)
4+
json_logic_ruby (0.2.4)
55
activesupport (~> 7.0)
66

77
GEM

lib/json_logic/concerns/trackable.rb

+22-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
module JsonLogic
44
module Trackable
5+
COMPLEX_OPERATORS = %w[and or if ! !! ?:].freeze
6+
57
attr_reader :tracker
68

79
def init_tracker(operator)
@@ -12,15 +14,34 @@ def init_tracker(operator)
1214
end
1315
end
1416

15-
def commit_rule_result!(var_name, operator, data, rules, result)
17+
def commit_rule_result!(operator, data, rules, result)
1618
if COMPLEX_OPERATORS.include?(operator)
1719
# change operand to parent & save result
1820
@tracker.result = result
1921
@tracker = @tracker.parent unless @tracker.parent.nil?
2022
return result
2123
end
24+
var_name = get_var_name(operator, rules)
2225
@tracker.add_data_point(var_name, operator, rules, data, result)
2326
result
2427
end
28+
29+
private
30+
31+
# This method retrieves the variable name from a hash of rules based on the given operator.
32+
# { "<=" : [ 25, { "var": "age" }, 75] }
33+
# { "<=" : [ { "var" : "age" }, 20 ] }
34+
#
35+
# @param operator [String] The operator for which to retrieve the variable name.
36+
# @param rules [Hash] A hash containing rule data.
37+
# @return [String, nil] The variable name if found, otherwise nil.
38+
39+
def get_var_name(operator, rules)
40+
args = rules[operator]
41+
index = COMPLEX_OPERATORS.exclude?(operator) && args.length == 3 ? 1 : 0
42+
args.dig(index, 'var')
43+
rescue TypeError
44+
nil
45+
end
2546
end
2647
end

lib/json_logic/evaluator.rb

+3-21
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ class Evaluator
55
include Trackable
66

77
def apply(rules, data = {})
8+
return rules.map { |val| apply(val, data) } if rules.is_a?(Array)
89
return rules unless rules.is_a?(Hash)
910

1011
operator = rules.keys[0]
1112
init_tracker(operator)
12-
13-
values = operator == 'map' ? [] : Array(rules[operator]).map { |rule| apply(rule, data) }
13+
values = operator == 'map' ? [] : apply(rules[operator], data)
1414

1515
operators(operator, values, rules, data)
1616
end
@@ -117,9 +117,7 @@ def json_logic_map(data, items_rule, map_rule)
117117

118118
def execute_operation(operator, rules, data, *)
119119
result = OPERATIONS[operator].call(*)
120-
var_name = get_var_name(operator, rules)
121-
122-
commit_rule_result!(var_name, operator, data, rules, result)
120+
commit_rule_result!(operator, data, rules, result)
123121
end
124122

125123
# This method retrieves the value of a variable with a given name from the data structure.
@@ -137,22 +135,6 @@ def get_var_value(data, var_name, default_value = nil)
137135
data.nil? ? default_value : data
138136
end
139137

140-
# This method retrieves the variable name from a hash of rules based on the given operator.
141-
# { "<=" : [ 25, { "var": "age" }, 75] }
142-
# { "<=" : [ { "var" : "age" }, 20 ] }
143-
#
144-
# @param operator [String] The operator for which to retrieve the variable name.
145-
# @param rules [Hash] A hash containing rule data.
146-
# @return [String, nil] The variable name if found, otherwise nil.
147-
148-
def get_var_name(operator, rules)
149-
args = rules[operator]
150-
index = COMPLEX_OPERATORS.exclude?(operator) && args.length == 3 ? 1 : 0
151-
args.dig(index, 'var')
152-
rescue TypeError
153-
nil
154-
end
155-
156138
def missing(data, *args)
157139
args.select { |arg| get_var_value(data, arg).nil? }
158140
end

lib/json_logic/operations.rb

-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

33
module JsonLogic
4-
COMPLEX_OPERATORS = %w[and or if].freeze
5-
64
OPERATIONS = {
75
'==' => ->(a, b) { a == b },
86
'!=' => ->(a, b) { a != b },

lib/json_logic/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# frozen_string_literal: true
22

33
module JsonLogic
4-
VERSION = '0.2.3'
4+
VERSION = '0.2.4'
55
end

spec/json_logic/evaluator_spec.rb

+120-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
RSpec.describe JsonLogic::Evaluator do
44
describe '#apply' do
5-
subject(:evaluator) { described_class.new.apply(rules, data) }
5+
subject(:evaluator) { logic.apply(rules, data) }
6+
7+
let(:logic) { described_class.new }
68

79
context 'with var' do
810
let(:rules) { { 'var' => var } }
@@ -152,6 +154,122 @@
152154

153155
it { is_expected.to eq(result) }
154156
end
157+
158+
context 'when not(!)' do
159+
context 'when not between' do
160+
let(:rules) { { '!' => { '<=' => [70, { 'var' => 'age' }, 75] } } }
161+
162+
context 'when main part is false' do
163+
let(:data) { { 'age' => 69 } }
164+
165+
it 'returns and tracks true' do
166+
expect(evaluator).to be(true)
167+
expect(logic.tracker.result).to be(true)
168+
end
169+
end
170+
171+
context 'when main part is true' do
172+
let(:data) { { 'age' => 72 } }
173+
174+
it 'returns and tracks false' do
175+
expect(evaluator).to be(false)
176+
expect(logic.tracker.result).to be(false)
177+
end
178+
end
179+
end
180+
181+
context 'when not one of (select not any in)' do
182+
context 'with string' do
183+
let(:rules) { { '!' => { 'in' => [{ 'var' => 'drink' }, 'sell cola'] } } }
184+
185+
context 'when main part is false' do
186+
let(:data) { { 'drink' => 'beer' } }
187+
188+
it 'returns and tracks true' do
189+
expect(evaluator).to be(true)
190+
expect(logic.tracker.result).to be(true)
191+
end
192+
end
193+
194+
context 'when main part is true' do
195+
let(:data) { { 'drink' => 'cola' } }
196+
197+
it 'returns and tracks false' do
198+
expect(evaluator).to be(false)
199+
expect(logic.tracker.result).to be(false)
200+
end
201+
end
202+
end
203+
204+
context 'with array' do
205+
let(:rules) { { '!' => { 'in' => [{ 'var' => 'drink' }, %w[cola juice]] } } }
206+
207+
context 'when main part is false' do
208+
let(:data) { { 'drink' => 'beer' } }
209+
210+
it 'returns and tracks true' do
211+
expect(evaluator).to be(true)
212+
expect(logic.tracker.result).to be(true)
213+
end
214+
end
215+
216+
context 'when main part is true' do
217+
let(:data) { { 'drink' => 'cola' } }
218+
219+
it 'returns and tracks false' do
220+
expect(evaluator).to be(false)
221+
expect(logic.tracker.result).to be(false)
222+
end
223+
end
224+
end
225+
end
226+
227+
context 'when does not contain any of (not like)' do
228+
context 'with string' do
229+
let(:rules) { { '!' => { 'in' => ['ol', { 'var' => 'drink' }] } } }
230+
231+
context 'when main part is false' do
232+
let(:data) { { 'drink' => 'juice' } }
233+
234+
it 'returns and tracks true' do
235+
expect(evaluator).to be(true)
236+
expect(logic.tracker.result).to be(true)
237+
end
238+
end
239+
240+
context 'when main part is true' do
241+
let(:data) { { 'drink' => 'cola' } }
242+
243+
it 'returns and tracks false' do
244+
expect(evaluator).to be(false)
245+
expect(logic.tracker.result).to be(false)
246+
end
247+
end
248+
end
249+
250+
context 'with array' do
251+
let(:rules) { { '!' => { 'in' => ['beer', { 'var' => 'drinks' }] } } }
252+
253+
context 'when main part is false' do
254+
let(:data) { { 'drinks' => %w[cola juice] } }
255+
256+
it 'returns and tracks true' do
257+
expect(evaluator).to be(true)
258+
expect(logic.tracker.result).to be(true)
259+
end
260+
end
261+
262+
context 'when main part is true' do
263+
let(:data) { { 'drinks' => %w[beer cola] } }
264+
265+
it 'returns and tracks false' do
266+
expect(evaluator).to be(false)
267+
expect(logic.tracker.result).to be(false)
268+
end
269+
end
270+
end
271+
end
272+
end
155273
end
156274

157275
describe '#get_var_name' do
@@ -308,7 +426,7 @@
308426
end
309427

310428
describe '#fetch_var_values' do
311-
subject { described_class.new.send(:fetch_var_values, rules, var_name) }
429+
subject { described_class.new.fetch_var_values(rules, var_name) }
312430

313431
context 'when variable present' do
314432
let(:rules) do

0 commit comments

Comments
 (0)