defmodule Rulex do @moduledoc """ Documentation for `Rulex`. """ require JSONPointer require JSON, as: Jason @doc """ evaluate/1 function consumes a map with schema: ```json { "data": {}, "rules": [] } ``` """ def evaluate(%{"data" => data, "rules" => rules}) when is_list(rules) do stream_rules(data, rules) end def evaluate(json_string) when is_binary(json_string) do {:ok, %{"data" => data, "rules" => rules}} = Jason.decode(json_string) stream_rules(data, rules) end @doc """ evaluate/2 consumes a json data payload + an array of rules """ def evaluate(data, rules) when is_list(rules) do stream_rules(data, rules) end def evaluate(data, rules) when is_binary(data) and is_binary(rules) do {:ok, data} = Jason.decode(data) {:ok, rules} = Jason.decode(rules) stream_rules(data, rules) end def stream_rules(data, rules) when is_list(rules) do Task.async_stream(rules, fn rule -> if match_rule?(data, rule) do rule end end) |> Enum.map(fn {:ok, result} -> result end) |> Enum.filter(& &1) end defp match_rule?(data, %{"conditions" => conditions}) do match_conditions?(data, conditions) end @doc false defp match_rule?(_, _), do: false defp match_conditions?(data, conditions) do case conditions do %{"all" => all} when is_list(all) -> Enum.all?(all, fn condition -> match_conditions?(data, condition) end) %{"any" => any} when is_list(any) -> Enum.any?(any, fn condition -> match_conditions?(data, condition) end) _ -> match_condition?(data, conditions) end end defp match_condition?(data, %{"path" => pointer, "operator" => operator, "value" => value}) do case JSONPointer.get(data, pointer) do {:ok, path} -> try do evaluate_path(path, operator, value) catch _ -> false end {:error, _} -> false end end @doc false defp match_condition?(_, _), do: false defp evaluate_path(path, "equals", value), do: path == value defp evaluate_path(path, "not_equal", value), do: path != value defp evaluate_path(path, "greater_than", value) when is_number(path), do: path > value defp evaluate_path(path, "less_than", value) when is_number(path), do: path < value defp evaluate_path(path, "contains", value) when is_binary(path), do: String.contains?(path, value) defp evaluate_path(path, "contains", value) when is_list(path), do: value in path defp evaluate_path(path, "contains", value) when is_map(path), do: Map.has_key?(path, value) defp evaluate_path(_path, "contains", _value), do: false defp evaluate_path(path, "not_contains", value) when is_binary(path), do: not String.contains?(path, value) defp evaluate_path(path, "not_contains", value) when is_list(path), do: value not in path defp evaluate_path(path, "not_contains", value) when is_map(path), do: not Map.has_key?(path, value) defp evaluate_path(_path, "not_contains", _value), do: false defp evaluate_path(path, "like", value) when is_binary(path) and is_binary(value), do: String.contains?(String.downcase(path), String.downcase(value)) defp evaluate_path(path, "like", value) do {:ok, like_path} = Jason.encode!(path) {:ok, like_value} = Jason.encode!(value) String.contains?(String.downcase(like_path), String.downcase(like_value)) end defp evaluate_path(path, "regex_match", value) when is_binary(path), do: Regex.match?(Regex.compile!(value), path) defp evaluate_path(_path, "regex_match", _value), do: false defp evaluate_path(_path, _operator, _value), do: false end