Current section
Files
Jump to
Current section
Files
lib/json_logic.ex
defmodule JsonLogic do
@moduledoc """
An Elixir implementation of [JsonLogic](http://jsonlogic.com/).
## Examples
iex> JsonLogic.apply(nil)
nil
iex> JsonLogic.apply(%{})
%{}
iex> JsonLogic.apply(%{"var" => "key"}, %{"key" => "value"})
"value"
iex> JsonLogic.apply(%{"var" => "nested.key"}, %{"nested" => %{"key" => "value"}})
"value"
iex> JsonLogic.apply(%{"var" => ["none", "default"]}, %{"key" => "value"})
"default"
iex> JsonLogic.apply(%{"var" => 0}, ~w{a b})
"a"
iex> JsonLogic.apply(%{"==" => [1, 1]})
true
iex> JsonLogic.apply(%{"==" => [0, 1]})
false
iex> JsonLogic.apply(%{"!=" => [1, 1]})
false
iex> JsonLogic.apply(%{"!=" => [0, 1]})
true
iex> JsonLogic.apply(%{"===" => [1, 1]})
true
iex> JsonLogic.apply(%{"===" => [1, 1.0]})
false
iex> JsonLogic.apply(%{"===" => [1, %{"var" => "key"}]}, %{"key" => 1})
true
iex> JsonLogic.apply(%{"!==" => [1, 1.0]})
true
iex> JsonLogic.apply(%{"!==" => [1, 1]})
false
iex> JsonLogic.apply(%{"!" => true})
false
iex> JsonLogic.apply(%{"!" => false})
true
iex> JsonLogic.apply(%{"if" => [true, "yes", "no" ]})
"yes"
iex> JsonLogic.apply(%{"if" => [false, "yes", "no" ]})
"no"
iex> JsonLogic.apply(%{"if" => [false, "unexpected", false, "unexpected", "default" ]})
"default"
iex> JsonLogic.apply(%{"or" => [false, nil, "truthy"]})
"truthy"
iex> JsonLogic.apply(%{"or" => ["first", "truthy"]})
"first"
iex> JsonLogic.apply(%{"and" => [false, "falsy"]})
false
iex> JsonLogic.apply(%{"and" => [true, 1, "truthy"]})
"truthy"
iex> JsonLogic.apply(%{"max" => [1,2,3]})
3
iex> JsonLogic.apply(%{"min" => [1,2,3]})
1
iex> JsonLogic.apply(%{"<" => [0, 1]})
true
iex> JsonLogic.apply(%{"<" => [1, 0]})
false
iex> JsonLogic.apply(%{"<" => [0, 1, 2]})
true
iex> JsonLogic.apply(%{"<" => [0, 2, 1]})
false
iex> JsonLogic.apply(%{">" => [1, 0]})
true
iex> JsonLogic.apply(%{">" => [0, 1]})
false
iex> JsonLogic.apply(%{">" => [2, 1, 0]})
true
iex> JsonLogic.apply(%{">" => [2, 0, 1]})
false
iex> JsonLogic.apply(%{"<=" => [1, 1]})
true
iex> JsonLogic.apply(%{"<=" => [1, 0]})
false
iex> JsonLogic.apply(%{"<=" => [1, 1, 2]})
true
iex> JsonLogic.apply(%{"<=" => [1, 0, 2]})
false
iex> JsonLogic.apply(%{">=" => [1, 1]})
true
iex> JsonLogic.apply(%{">=" => [0, 1]})
false
iex> JsonLogic.apply(%{">=" => [1, 1, 0]})
true
iex> JsonLogic.apply(%{">=" => [0, 1, 2]})
false
iex> JsonLogic.apply(%{"+" => [1,2,3]})
6
iex> JsonLogic.apply(%{"+" => [2]})
2
iex> JsonLogic.apply(%{"-" => [7,4]})
3
iex> JsonLogic.apply(%{"-" => [2]})
-2
iex> JsonLogic.apply(%{"*" => [2,3,4]})
24
iex> JsonLogic.apply(%{"/" => [5,2]})
2.5
iex> JsonLogic.apply(%{"%" => [7, 3]})
1
iex> JsonLogic.apply(%{"map" => [ [1,2,3,4,5], %{"*" => [%{"var" => ""}, 2]} ]})
[2,4,6,8,10]
iex> JsonLogic.apply(%{"filter" => [ [1,2,3,4,5], %{">" => [%{"var" => ""}, 2]} ]})
[3,4,5]
iex> JsonLogic.apply(%{"reduce" => [ [1,2,3,4,5], %{"+" => [%{"var" => "current"}, %{"var" => "accumulator"}]} ]})
15
iex> JsonLogic.apply(%{"all" => [ [1,2,3], %{">" => [ %{"var" => ""}, 0 ]} ]})
true
iex> JsonLogic.apply(%{"all" => [ [-1,2,3], %{">" => [ %{"var" => ""}, 0 ]} ]})
false
iex> JsonLogic.apply(%{"none" => [ [1,2,3], %{"<" => [ %{"var" => ""}, 0 ]} ]})
true
iex> JsonLogic.apply(%{"none" => [ [-1,2,3], %{"<" => [ %{"var" => ""}, 0 ]} ]})
false
iex> JsonLogic.apply(%{"some" => [ [-1,2,3], %{"<" => [ %{"var" => ""}, 0 ]} ]})
true
iex> JsonLogic.apply(%{"some" => [ [1,2,3], %{"<" => [ %{"var" => ""}, 0 ]} ]})
false
iex> JsonLogic.apply(%{"in" => ["sub", "substring"]})
true
iex> JsonLogic.apply(%{"in" => ["na", "substring"]})
false
iex> JsonLogic.apply(%{"in" => ["a", ["a", "b", "c"]]})
true
iex> JsonLogic.apply(%{"in" => ["z", ["a", "b", "c"]]})
false
iex> JsonLogic.apply(%{"cat" => ["a", "b", "c"]})
"abc"
iex> JsonLogic.apply(%{"log" => "string"})
"string"
"""
@operations %{
"var" => :operation_var,
"if" => :operation_if,
"==" => :operation_similar,
"!=" => :operation_not_similar,
"===" => :operation_equal,
"!==" => :operation_not_equal,
"!" => :operation_not,
"or" => :operation_or,
"and" => :operation_and,
"<" => :operation_less_than,
">" => :operation_greater_than,
"<=" => :operation_less_than_or_equal,
">=" => :operation_greater_than_or_equal,
"max" => :operation_max,
"min" => :operation_min,
"+" => :operation_addition,
"-" => :operation_subtraction,
"*" => :operation_multiplication,
"/" => :operation_division,
"%" => :operation_remainder,
"map" => :operation_map,
"filter" => :operation_filter,
"reduce" => :operation_reduce,
"all" => :operation_all,
"none" => :operation_none,
"some" => :operation_some,
"in" => :operation_in,
"cat" => :operation_cat,
"log" => :operation_log,
}
@doc """
Apply JsonLogic.
Accepts logic and data arguments as Map
Returns resolved result as Map
"""
@spec apply(Map.t, Map.t) :: Map.t
def apply(logic, data \\ nil)
# operations selector branch of apply
def apply(logic, data) when is_map(logic) and logic != %{} do
operation_name = logic |> Map.keys |> List.first
values = logic |> Map.values |> List.first
Kernel.apply(__MODULE__, @operations[operation_name], [values, data])
end
# conclusive branch of apply
def apply(logic, _) do
logic
end
@doc false
def operation_var("", data) do
data
end
@doc false
# TODO: may need refactoring
def operation_var(path, data) when is_binary(path) do
[variable_name | names] = path |> String.split(".")
fetched_data = if is_list(data) do
{index, _} = Integer.parse(variable_name)
Enum.at(data, index)
else
data[variable_name]
end
case names do
[] -> fetched_data
_ -> operation_var(names |> Enum.join("."), fetched_data)
end
end
@doc false
def operation_var([path, default_key], data) do
operation_var(path, data) || JsonLogic.apply(default_key, data)
end
@doc false
def operation_var(index, data) when is_number(index) do
Enum.at(data, index)
end
@doc false
def operation_similar([left, right], data \\ nil) do
JsonLogic.apply(left, data) == JsonLogic.apply(right, data)
end
@doc false
def operation_not_similar([left, right], data \\ nil) do
JsonLogic.apply(left, data) != JsonLogic.apply(right, data)
end
@doc false
def operation_equal([left, right], data \\ nil) do
JsonLogic.apply(left, data) === JsonLogic.apply(right, data)
end
@doc false
def operation_not_equal([left, right], data \\ nil) do
JsonLogic.apply(left, data) !== JsonLogic.apply(right, data)
end
@doc false
def operation_not(condition, data) do
case condition do
[condition] -> !JsonLogic.apply(condition, data)
condition -> !JsonLogic.apply(condition, data)
end
end
@doc false
def operation_if([last], data) do
JsonLogic.apply(last, data)
end
@doc false
def operation_if([condition, yes], data) do
Kernel.if JsonLogic.apply(condition, data), do: JsonLogic.apply(yes, data), else: nil
end
@doc false
def operation_if([condition, yes, no], data) do
Kernel.if JsonLogic.apply(condition, data), do: JsonLogic.apply(yes, data), else: JsonLogic.apply(no, data)
end
@doc false
def operation_if([condition, yes | others], data) do
Kernel.if JsonLogic.apply(condition, data), do: JsonLogic.apply(yes, data), else: operation_if(others, data)
end
@doc false
def operation_or(statements, data) do
[first | others] = statements
JsonLogic.apply(first, data) || operation_or(others, data)
end
@doc false
def operation_and(statements, data) do
[first | others] = statements
first_result = JsonLogic.apply(first, data)
if first_result && others != [] do
operation_and(others, data)
else
first_result
end
end
@doc false
def operation_max(list, data) do
list |> Enum.map(fn(x) -> JsonLogic.apply(x, data) end) |> Enum.max
end
@doc false
def operation_min(list, data) do
list |> Enum.map(fn(x) -> JsonLogic.apply(x, data) end) |> Enum.min
end
@doc false
def operation_less_than([left, right], data) do
JsonLogic.apply(left, data) < JsonLogic.apply(right, data)
end
@doc false
def operation_less_than([left, middle, right | _], data) do
JsonLogic.apply(left, data) < JsonLogic.apply(middle, data) &&
JsonLogic.apply(middle, data) < JsonLogic.apply(right, data)
end
@doc false
def operation_greater_than([left, right], data) do
JsonLogic.apply(left, data) > JsonLogic.apply(right, data)
end
@doc false
def operation_greater_than([left, middle, right | _], data) do
JsonLogic.apply(left, data) > JsonLogic.apply(middle, data) &&
JsonLogic.apply(middle, data) > JsonLogic.apply(right, data)
end
@doc false
def operation_less_than_or_equal([left, right], data) do
JsonLogic.apply(left, data) <= JsonLogic.apply(right, data)
end
@doc false
def operation_less_than_or_equal([left, middle, right | _], data) do
JsonLogic.apply(left, data) <= JsonLogic.apply(middle, data) &&
JsonLogic.apply(middle, data) <= JsonLogic.apply(right, data)
end
@doc false
def operation_greater_than_or_equal([left, right], data) do
JsonLogic.apply(left, data) >= JsonLogic.apply(right, data)
end
@doc false
def operation_greater_than_or_equal([left, middle, right | _], data) do
JsonLogic.apply(left, data) >= JsonLogic.apply(middle, data) &&
JsonLogic.apply(middle, data) >= JsonLogic.apply(right, data)
end
@doc false
def operation_addition(numbers, data) do
[first | rest] = numbers
reduce_from = JsonLogic.apply(first, data)
reduce_on = Enum.map(rest, fn(n) -> JsonLogic.apply(n, data) end)
{_, result} = Enum.map_reduce(reduce_on, reduce_from, fn(n, total) -> {n, total + n} end)
result
end
@doc false
def operation_subtraction([first, last], data) do
reduce_on = [JsonLogic.apply(last, data)]
reduce_from = JsonLogic.apply(first, data)
{_, result} = Enum.map_reduce(reduce_on, reduce_from, fn(n, total) -> {n, total - n} end)
result
end
@doc false
def operation_subtraction([first], data) do
-JsonLogic.apply(first, data)
end
@doc false
def operation_multiplication([first | rest], data) do
reduce_on = Enum.map(rest, fn(n) -> JsonLogic.apply(n, data) end)
reduce_from = JsonLogic.apply(first, data)
{_, result} = Enum.map_reduce(reduce_on, reduce_from, fn(n, total) -> {n, total * n} end)
result
end
@doc false
def operation_division([first, last], data) do
reduce_on = [JsonLogic.apply(last, data)]
reduce_from = JsonLogic.apply(first, data)
{_, result} = Enum.map_reduce(reduce_on, reduce_from, fn(n, total) -> {n, total / n} end)
result
end
@doc false
def operation_remainder([first, last], data) do
Kernel.rem(JsonLogic.apply(first, data), JsonLogic.apply(last, data))
end
@doc false
def operation_map([list, map_action], data) do
JsonLogic.apply(list, data)
|> Enum.map(fn(item) -> JsonLogic.apply(map_action, item) end)
end
@doc false
def operation_filter([list, filter_action], data) do
JsonLogic.apply(list, data)
|> Enum.filter(fn(item) -> JsonLogic.apply(filter_action, item) end)
end
@doc false
def operation_reduce([list, reduce_action], data) do
[first | others] = JsonLogic.apply(list, data)
others |> Enum.reduce(first, fn(item, accumulator) ->
JsonLogic.apply(reduce_action, %{"current" => item, "accumulator" => accumulator})
end)
end
@doc false
def operation_all([list, test], data) do
JsonLogic.apply(list, data)
|> Enum.all?(fn(item) -> JsonLogic.apply(test, item) end)
end
@doc false
def operation_none([list, test], data) do
JsonLogic.apply(list, data)
|> Enum.all?(fn(item) -> Kernel.if(JsonLogic.apply(test, item), do: false, else: true) end)
end
@doc false
def operation_some([list, test], data) do
JsonLogic.apply(list, data)
|> Enum.any?(fn(item) -> JsonLogic.apply(test, item) end)
end
@doc false
def operation_in([member, list], data) when is_list(list) do
members = list |> Enum.map(fn(m) -> JsonLogic.apply(m, data) end)
Enum.member?(members, JsonLogic.apply(member, data))
end
@doc false
def operation_in([substring, string], data) when is_binary(string) do
String.contains?(string, JsonLogic.apply(substring, data))
end
@doc false
def operation_in([_, from], _) when is_nil(from) do
false
end
@doc false
def operation_in([find, from], data) do
operation_in([JsonLogic.apply(find, data), JsonLogic.apply(from, data)], data)
end
@doc false
def operation_cat(strings, data) do
strings |> Enum.map(fn(s) -> JsonLogic.apply(s, data) end) |> Enum.join
end
@doc false
def operation_log(logic, data) do
JsonLogic.apply(logic, data)
end
end