lib/judge_json.ex

defmodule JudgeJson do
  @moduledoc """
  Documentation for `JudgeJson`.
  """
  require JSONPointer
  require 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