Packages

A flexible rule engine library for Elixir that allows you to define and execute business rules using JSON-based configurations.

Current section

Files

Jump to
excanon lib operation_evaluator.ex
Raw

lib/operation_evaluator.ex

defmodule OperationEvaluator do
@moduledoc false
alias JSONPointer
# Logic operators
def evaluate(facts, %{"eq" => [head | tail] = data}) when is_list(data) do
if length(data) < 2 do
raise ArgumentError, "minimum arguments not met"
end
evaluated_head = evaluate(facts, head)
Enum.all?(tail, fn val -> evaluate(facts, val) == evaluated_head end)
end
def evaluate(facts, %{"neq" => data}) when is_list(data) do
!evaluate(facts, %{"eq" => data})
end
def evaluate(facts, %{"and" => data}) when is_list(data) do
if length(data) < 2 do
raise ArgumentError, "minimum arguments not met"
end
Enum.all?(data, fn val -> evaluate(facts, val) end)
end
def evaluate(facts, %{"or" => data}) when is_list(data) do
if length(data) < 2 do
raise ArgumentError, "minimum arguments not met"
end
Enum.any?(data, fn val -> evaluate(facts, val) end)
end
def evaluate(facts, %{"gt" => [op1, op2]}) do
evaluate(facts, op1) > evaluate(facts, op2)
end
def evaluate(facts, %{"gte" => [op1, op2]}) do
evaluate(facts, op1) >= evaluate(facts, op2)
end
def evaluate(facts, %{"lt" => [op1, op2]}) do
evaluate(facts, op1) < evaluate(facts, op2)
end
def evaluate(facts, %{"lte" => [op1, op2]}) do
evaluate(facts, op1) <= evaluate(facts, op2)
end
# Arithmetic operators
def evaluate(facts, %{"plus" => data}) when is_list(data) do
if length(data) < 2 do
raise ArgumentError, "minimum arguments not met"
end
data
|> Enum.map(fn val -> evaluate(facts, val) end)
|> Enum.sum()
end
def evaluate(facts, %{"minus" => [first | subs]}) do
if subs == [] do
raise ArgumentError, "minimum arguments not met"
end
sub =
subs
|> Enum.map(fn val -> evaluate(facts, val) end)
|> Enum.sum()
evaluate(facts, first) - sub
end
def evaluate(facts, %{"div" => [op1, op2]}) do
evaluate(facts, op1) / evaluate(facts, op2)
end
def evaluate(facts, %{"mult" => data}) when is_list(data) do
if length(data) < 2 do
raise ArgumentError, "minimum arguments not met"
end
data
|> Enum.map(fn val -> evaluate(facts, val) end)
|> Enum.product()
end
def evaluate(facts, %{"mod" => [op1, op2]}) do
rem(evaluate(facts, op1), evaluate(facts, op2))
end
# Fact references
def evaluate(facts, %{"obj" => path}) when is_binary(path) do
pointer_formatted = to_json_pointer(path)
case JSONPointer.get(facts, pointer_formatted) do
{:ok, result} -> result
{:error, message} -> raise ArgumentError, message
end
end
# Script caller
def evaluate(_, %{"call" => file}) when is_binary(file) do
{result, _} = Code.eval_file(file)
result
end
# Setter operation
def evaluate(facts, %{"set" => [key, value]}) when is_binary(key) do
evaluated_value = evaluate(facts, value)
formatted_key = to_json_pointer(key)
case JSONPointer.set(facts, formatted_key, evaluated_value) do
{:ok, modified_facts, _} -> {:ok, modified_facts}
{:error, message} -> raise ArgumentError, message
end
end
# Constant assigments
def evaluate(_, val) when is_number(val), do: val
def evaluate(_, val) when is_binary(val), do: val
def evaluate(_, val) when is_boolean(val), do: val
def evaluate(_, val) when is_list(val), do: val
def evaluate(_, val) when is_nil(val), do: val
def evaluate(_, _), do: raise(ArgumentError)
defp to_json_pointer(path) do
array_reference_transformed = Regex.replace(~r/\[(\d+)\]/, path, "\.\\\1")
tokenized = String.split(array_reference_transformed, ".")
"/" <> Enum.join(tokenized, "/")
end
end