Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/core_ext/stringify_keys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ class Hash
# Stolen from ActiveSupport
def transform_keys
return enum_for(:transform_keys) { size } unless block_given?

result = {}
each_key do |key|
result[yield(key)] = self[key]
end

result
end

Expand Down
8 changes: 2 additions & 6 deletions lib/json_logic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@
module JSONLogic
def self.apply(logic, data)
if logic.is_a?(Array)
logic.map do |val|
apply(val, data)
end
logic.map { |val| apply(val, data) }
elsif !logic.is_a?(Hash)
# Pass-thru
logic
else
if data.is_a?(Hash)
data = data.stringify_keys
end
data = data.stringify_keys if data.is_a?(Hash)
data ||= {}

operator, values = operator_and_values_from_logic(logic)
Expand Down
58 changes: 24 additions & 34 deletions lib/json_logic/operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,17 @@ class Operation
d
else
if v == [JSONLogic::ITERABLE_KEY]
if d.is_a?(Array)
d
else
d[JSONLogic::ITERABLE_KEY]
end
d.is_a?(Array) ? d : d[JSONLogic::ITERABLE_KEY]
else
d.deep_fetch(*v)
end
end
end,
'missing' => ->(v, d) { v.select { |val| d.deep_fetch(val).nil? } },
'missing_some' => ->(v, d) {
'missing_some' => ->(v, d) do
present = v[1] & d.keys
present.size >= v[0] ? [] : LAMBDAS['missing'].call(v[1], d)
},
end,
'some' => -> (v,d) do
return false unless v[0].is_a?(Array)

Expand All @@ -38,27 +34,14 @@ class Operation
end
end,
'substr' => -> (v,d) do
limit = -1
if v[2]
if v[2] < 0
limit = v[2] - 1
else
limit = v[1] + v[2] - 1
end
end
return v[0][v[1]..-1] unless v[2]

v[0][v[1]..limit]
limit = v[2] < 0 ? v[2] - 1 : v[1] + v[2] - 1

v[0][v[1]..limit]
end,
'none' => -> (v,d) do

v[0].each do |val|
this_val_satisfies_condition = interpolated_block(v[1], val)
if this_val_satisfies_condition
return false
end
end

return true
v[0].none? { |val| interpolated_block(v[1], val) }
end,
'all' => -> (v,d) do
# Difference between Ruby and JSONLogic spec ruby all? with empty array is true
Expand All @@ -70,31 +53,35 @@ class Operation
end,
'reduce' => -> (v,d) do
return v[2] unless v[0].is_a?(Array)
v[0].inject(v[2]) { |acc, val| interpolated_block(v[1], { "current": val, "accumulator": acc })}

v[0].inject(v[2]) do |acc, val|
interpolated_block(v[1], { "current": val, "accumulator": acc })
end
end,
'map' => -> (v,d) do
return [] unless v[0].is_a?(Array)

v[0].map do |val|
interpolated_block(v[1], val)
end
end,
'if' => ->(v, d) {
'if' => ->(v, d) do
v.each_slice(2) do |condition, value|
return condition if value.nil?
return value if condition.truthy?
end
},
end,
'==' => ->(v, d) { v[0].to_s == v[1].to_s },
'===' => ->(v, d) { v[0] == v[1] },
'!=' => ->(v, d) { v[0].to_s != v[1].to_s },
'!==' => ->(v, d) { v[0] != v[1] },
'!' => ->(v, d) { v[0].falsy? },
'!!' => ->(v, d) { v[0].truthy? },
'or' => ->(v, d) { v.find(&:truthy?) || v.last },
'and' => ->(v, d) {
'and' => ->(v, d) do
result = v.find(&:falsy?)
result.nil? ? v.last : result
},
end,
'?:' => ->(v, d) { LAMBDAS['if'].call(v, d) },
'>' => ->(v, d) { v.map(&:to_f).each_cons(2).all? { |i, j| i > j } },
'>=' => ->(v, d) { v.map(&:to_f).each_cons(2).all? { |i, j| i >= j } },
Expand All @@ -109,7 +96,10 @@ class Operation
'%' => ->(v, d) { v.map(&:to_i).reduce(:%) },
'^' => ->(v, d) { v.map(&:to_f).reduce(:**) },
'merge' => ->(v, d) { v.flatten },
'in' => ->(v, d) { interpolated_block(v[1], d).include? v[0] },
'in' => ->(v, d) do
result = interpolated_block(v[1], d)&.include? v[0]
result.nil? ? false : result
end,
'cat' => ->(v, d) { v.map(&:to_s).join },
'log' => ->(v, d) { puts v }
}
Expand All @@ -123,10 +113,10 @@ def self.perform(operator, values, data)
# If iterable, we can only pre-fill the first element, the second one must be evaluated per element.
# If not, we can prefill all.

if is_iterable?(operator)
interpolated = [JSONLogic.apply(values[0], data), *values[1..-1]]
interpolated = if is_iterable?(operator)
[JSONLogic.apply(values[0], data), *values[1..-1]]
else
interpolated = values.map { |val| JSONLogic.apply(val, data) }
values.map { |val| JSONLogic.apply(val, data) }
end

interpolated.flatten!(1) if interpolated.size == 1 # [['A']] => ['A']
Expand Down
64 changes: 35 additions & 29 deletions test/json_logic_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,30 @@ class JSONLogicTest < Minitest::Test
def test_filter
filter = JSON.parse(%Q|{">": [{"var": "id"}, 1]}|)
data = JSON.parse(%Q|[{"id": 1},{"id": 2}]|)
assert_equal([{'id' => 2}], JSONLogic.filter(filter, data))

assert_equal([{ 'id' => 2 }], JSONLogic.filter(filter, data))
end

def test_symbol_operation
logic = {'==': [{var: "id"}, 1]}
logic = {'==': [{ var: "id" }, 1]}
data = JSON.parse(%Q|{"id": 1}|)
assert_equal(true, JSONLogic.apply(logic, data))

assert JSONLogic.apply(logic, data)
end

def test_false_value
logic = {'==': [{var: "flag"}, false]}
logic = { '==': [{ var: "flag" }, false] }
data = JSON.parse(%Q|{"flag": false}|)
assert_equal(true, JSONLogic.apply(logic, data))

assert JSONLogic.apply(logic, data)
end

def test_add_operation
new_operation = ->(v, d) { v.map { |x| x + 5 } }
JSONLogic.add_operation('fives', new_operation)
rules = JSON.parse(%Q|{"fives": {"var": "num"}}|)
data = JSON.parse(%Q|{"num": 1}|)

assert_equal([6], JSONLogic.apply(rules, data))
end

Expand All @@ -63,44 +67,44 @@ def test_exponent_operation
end

def test_array_with_logic
assert_equal [1, 2, 3], JSONLogic.apply([1, {"var" => "x"}, 3], {"x" => 2})
assert_equal [1, 2, 3], JSONLogic.apply([1, { "var" => "x" }, 3], { "x" => 2 })

assert_equal [42], JSONLogic.apply(
{
"if" => [
{"var" => "x"},
[{"var" => "y"}],
{ "var" => "x" },
[{ "var" => "y" }],
99
]
},
{ "x" => true, "y" => 42}
{ "x" => true, "y" => 42 }
)
end

def test_in_with_variable
assert_equal true, JSONLogic.apply(
assert JSONLogic.apply(
{
"in" => [
{"var" => "x"},
{"var" => "x"}
{ "var" => "x" },
{ "var" => "x" }
]
},
{ "x" => "foo"}
{ "x" => "foo" }
)

assert_equal false, JSONLogic.apply(
refute JSONLogic.apply(
{
"in" => [
{"var" => "x"},
{"var" => "y"},
{ "var" => "x" },
{ "var" => "y" }
]
},
{ "x" => "foo", "y" => "bar" }
)
end

def test_filter_with_non_array
assert_equal [], JSONLogic.apply(
assert_empty JSONLogic.apply(
{
"filter" => [
{ "var" => "x" },
Expand All @@ -115,8 +119,8 @@ def test_uses_data
assert_equal ["x", "y"], JSONLogic.uses_data(
{
"in" => [
{"var" => "x"},
{"var" => "y"},
{ "var" => "x" },
{ "var" => "y" }
]
}
)
Expand All @@ -126,21 +130,23 @@ def test_uses_data_missing
vars = JSONLogic.uses_data(
{
"in" => [
{"var" => "x"},
{"var" => "y"},
{ "var" => "x" },
{ "var" => "y" }
]
}
)

provided_data_missing_y = {
x: 3,
}
provided_data_missing_y = { x: 3 }
provided_data_missing_x = { y: 4 }

assert_equal ["y"], JSONLogic.apply({ "missing": [vars] }, provided_data_missing_y)
assert_equal ["x"], JSONLogic.apply({ "missing": [vars] }, provided_data_missing_x)
end

provided_data_missing_x = {
y: 4,
}
def test_in_with_non_array
logic = { "in" => ["searchable_elem", { "var" => "non_array" }] }

assert_equal ["y"], JSONLogic.apply({"missing": [vars]}, provided_data_missing_y)
assert_equal ["x"], JSONLogic.apply({"missing": [vars]}, provided_data_missing_x)
refute JSONLogic.apply(logic, { "non_array" => nil })
refute JSONLogic.apply(logic, nil)
end
end