Skip to content

Commit 63205e3

Browse files
Merge pull request #4 from Kaligo/feature/logic-compiler
Add logic compiler
2 parents ff50514 + afd3730 commit 63205e3

File tree

4 files changed

+69
-77
lines changed

4 files changed

+69
-77
lines changed

lib/core_ext/deep_fetch.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ def deep_fetch(keys, default = nil)
1818

1919
class Hash
2020
def deep_fetch(keys, default = nil)
21-
value = dig(*keys) rescue default
21+
value = keys.inject(self) do |memo, item|
22+
memo.key?(item) ? memo[item] : memo[item.to_sym]
23+
rescue
24+
default
25+
end
2226
value.nil? ? default : value # value can be false (Boolean)
2327
end
2428
end

lib/core_ext/stringify_keys.rb

-21
This file was deleted.

lib/json_logic.rb

+47-14
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,60 @@
11
require 'core_ext/deep_fetch'
2-
require 'core_ext/stringify_keys'
32
require 'json_logic/truthy'
43
require 'json_logic/operation'
54
require 'json_logic/var_cache'
65

76
module JSONLogic
8-
def self.apply(logic, data)
9-
if logic.is_a?(Array)
10-
logic.map do |val|
11-
apply(val, data)
7+
8+
def self.compile(logic)
9+
klass = Class.new { define_method(:to_s) { logic.to_s } }
10+
11+
case logic
12+
when Array
13+
14+
compiled_values = logic.map { |item| JSONLogic.compile(item) }
15+
16+
klass.define_method(:evaluate) do |data|
17+
compiled_values.map { |item| item.evaluate(data) }
1218
end
13-
elsif !logic.is_a?(Hash)
14-
# Pass-thru
15-
logic
16-
else
17-
if data.is_a?(Hash)
18-
data = data.stringify_keys
19+
20+
when Hash
21+
22+
operation, values = logic.first
23+
values = [values] unless values.is_a?(Array)
24+
25+
compiled_values = values.map do |value|
26+
JSONLogic.compile(value)
1927
end
20-
data ||= {}
2128

22-
operator, values = operator_and_values_from_logic(logic)
23-
Operation.perform(operator, values, data)
29+
klass.define_method(:evaluate) do |data|
30+
evaluated_values =
31+
case operation
32+
when 'filter', 'some', 'none', 'all', 'map'
33+
input = compiled_values[0].evaluate(data)
34+
params = input&.map { |item| compiled_values[1].evaluate(item) }
35+
[input, params]
36+
when 'reduce'
37+
input = compiled_values[0].evaluate(data)
38+
accumulator = compiled_values[2].evaluate(data)
39+
[input, compiled_values[1], accumulator]
40+
else
41+
compiled_values.map { |item| item.evaluate(data) }
42+
end
43+
44+
Operation.perform(operation, evaluated_values, data)
45+
end
46+
47+
else
48+
49+
klass.define_method(:evaluate) { |_data| logic }
50+
2451
end
52+
53+
klass.new
54+
end
55+
56+
def self.apply(logic, data)
57+
compile(logic).evaluate(data)
2558
end
2659

2760
# Return a list of the non-literal data used. Eg, if the logic contains a {'var' => 'bananas'} operation, the result of

lib/json_logic/operation.rb

+17-41
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class Operation
1818
end
1919
end,
2020
'missing' => ->(v, d) do
21-
v.select do |val|
21+
v.flatten.select do |val|
2222
keys = VarCache.fetch_or_store(val)
2323
d.deep_fetch(keys).nil?
2424
end
@@ -30,15 +30,15 @@ class Operation
3030
'some' => -> (v,d) do
3131
return false unless v[0].is_a?(Array)
3232

33-
v[0].any? do |val|
34-
interpolated_block(v[1], val).truthy?
33+
v[1].any? do |item|
34+
item.truthy?
3535
end
3636
end,
3737
'filter' => -> (v,d) do
3838
return [] unless v[0].is_a?(Array)
3939

40-
v[0].select do |val|
41-
interpolated_block(v[1], val).truthy?
40+
v[0].select.with_index do |_, index|
41+
v[1][index].truthy?
4242
end
4343
end,
4444
'substr' => -> (v,d) do
@@ -54,33 +54,28 @@ class Operation
5454
v[0][v[1]..limit]
5555
end,
5656
'none' => -> (v,d) do
57+
return false unless v[0].is_a?(Array)
5758

58-
v[0].each do |val|
59-
this_val_satisfies_condition = interpolated_block(v[1], val)
60-
if this_val_satisfies_condition
61-
return false
62-
end
59+
v[1].all? do |item|
60+
item.falsy?
6361
end
64-
65-
return true
6662
end,
6763
'all' => -> (v,d) do
64+
return false unless v[0].is_a?(Array)
6865
# Difference between Ruby and JSONLogic spec ruby all? with empty array is true
6966
return false if v[0].empty?
7067

71-
v[0].all? do |val|
72-
interpolated_block(v[1], val)
68+
v[1].all? do |item|
69+
item.truthy?
7370
end
7471
end,
7572
'reduce' => -> (v,d) do
7673
return v[2] unless v[0].is_a?(Array)
77-
v[0].inject(v[2]) { |acc, val| interpolated_block(v[1], { "current": val, "accumulator": acc })}
74+
v[0].inject(v[2]) { |acc, val| v[1].evaluate({ "current" => val, "accumulator" => acc })}
7875
end,
7976
'map' => -> (v,d) do
8077
return [] unless v[0].is_a?(Array)
81-
v[0].map do |val|
82-
interpolated_block(v[1], val)
83-
end
78+
v[1]
8479
end,
8580
'if' => ->(v, d) {
8681
v.each_slice(2) do |condition, value|
@@ -115,42 +110,23 @@ class Operation
115110
'%' => ->(v, d) { v.map(&:to_i).reduce(:%) },
116111
'^' => ->(v, d) { v.map(&:to_f).reduce(:**) },
117112
'merge' => ->(v, d) { v.flatten },
118-
'in' => ->(v, d) { interpolated_block(v[1], d).include? v[0] },
113+
'in' => ->(v, d) { v[1].include?(v[0]) },
119114
'cat' => ->(v, d) { v.map(&:to_s).join },
120115
'log' => ->(v, d) { puts v }
121116
}
122117

123-
def self.interpolated_block(block, data)
124-
# Make sure the empty var is there to be used in iterator
125-
JSONLogic.apply(block, data.is_a?(Hash) ? data.merge({"": data}) : { "": data })
126-
end
127-
128118
def self.perform(operator, values, data)
129-
# If iterable, we can only pre-fill the first element, the second one must be evaluated per element.
130-
# If not, we can prefill all.
131-
132-
if is_iterable?(operator)
133-
interpolated = [JSONLogic.apply(values[0], data), *values[1..-1]]
119+
if is_standard?(operator)
120+
LAMBDAS[operator.to_s].call(values, data)
134121
else
135-
interpolated = values.map { |val| JSONLogic.apply(val, data) }
122+
send(operator, values, data)
136123
end
137-
138-
interpolated.flatten!(1) if interpolated.size == 1 # [['A']] => ['A']
139-
140-
return LAMBDAS[operator.to_s].call(interpolated, data) if is_standard?(operator)
141-
send(operator, interpolated, data)
142124
end
143125

144126
def self.is_standard?(operator)
145127
LAMBDAS.key?(operator.to_s)
146128
end
147129

148-
# Determine if values associated with operator need to be re-interpreted for each iteration(ie some kind of iterator)
149-
# or if values can just be evaluated before passing in.
150-
def self.is_iterable?(operator)
151-
['filter', 'some', 'all', 'none', 'in', 'map', 'reduce'].include?(operator.to_s)
152-
end
153-
154130
def self.add_operation(operator, function)
155131
self.class.send(:define_method, operator) do |v, d|
156132
function.call(v, d)

0 commit comments

Comments
 (0)