lib/loupe/stream/comparator.ex

defmodule Loupe.Stream.Comparator do
  @moduledoc """
  Behaviour to implement comparator. It could be useful to
  implement your own comparator to alter how fields are getting
  compared. 

  Overriding the comparator can allow someone to implement
  variant casting (exmaple `field:upper` to uppercase values
  automatically), alter the comparison logic or implement
  sigil casting.
  """

  alias Loupe.Language.Ast
  alias Loupe.Stream.Context

  @doc "Compares a stream's value with a literal value"
  @callback compare(Loupe.Language.Ast.operator()) :: boolean()

  @doc """
  Applies a field variant on a value. This can be used to have
  expression like `value:upper` be automatically uppercased.
  """
  @callback apply_variant(any(), String.t()) :: any()

  @doc """
  Casts a sigil to kind of value to be compared.
  """
  @callback cast_sigil(char(), String.t()) :: any()

  @doc "Compares predicates inside a given map/structure tree"
  @spec compare(Ast.predicate(), any(), Context.t()) :: boolean()
  def compare({:and, left, right}, element, %Context{} = context) do
    compare(left, element, context) and compare(right, element, context)
  end

  def compare({:or, left, right}, element, %Context{} = context) do
    compare(left, element, context) or compare(right, element, context)
  end

  def compare({:not, operand}, element, %Context{} = context) do
    case compute_expression(operand, element, context) do
      :empty -> false
      other -> not other
    end
  end

  def compare(operand, element, %Context{} = context) do
    with :empty <- compute_expression(operand, element, context) do
      false
    end
  end

  defp compute_expression(operand, element, %Context{} = context) do
    operand
    |> unwrap_right_value(context)
    |> compare_value(element, context)
  end

  defp unwrap_right_value({operator, left, right}, context) do
    {operator, left, unwrap_right_value(right, context)}
  end

  defp unwrap_right_value(:empty, _), do: nil
  defp unwrap_right_value(boolean, _) when is_boolean(boolean), do: boolean
  defp unwrap_right_value({:int, int}, _), do: int
  defp unwrap_right_value({:string, string}, _), do: string
  defp unwrap_right_value({:float, float}, _), do: float

  defp unwrap_right_value({:identifier, identifier}, %Context{variables: variables}) do
    Map.get(variables, identifier)
  end

  defp compare_value(_operand, [], _context) do
    :empty
  end

  defp compare_value(operand, elements, context) when is_list(elements) do
    Enum.any?(elements, &compare_value(operand, &1, context))
  end

  defp compare_value({operator, {:binding, {:or_binding, bindings}}, right}, element, context) do
    Enum.any?(bindings, fn binding ->
      compare_value({operator, {:binding, binding}, right}, element, context)
    end)
  end

  defp compare_value({operator, {:binding, {:and_binding, bindings}}, right}, element, context) do
    Enum.all?(bindings, fn binding ->
      compare_value({operator, {:binding, binding}, right}, element, context)
    end)
  end

  defp compare_value(
         {operator, {:binding, [{:path, paths} | rest_bindings]}, right},
         element,
         context
       ) do
    compare_value(
      {operator, {:binding, paths ++ rest_bindings}, right},
      element,
      context
    )
  end

  defp compare_value(
         {operator, {:binding, [binding | rest_bindings]}, right},
         element,
         %Context{comparator: comparator} = context
       ) do
    result =
      case {get_value(element, binding, context), rest_bindings} do
        {{:ok, value}, []} ->
          {operator, value, right}

        {{:ok, value}, _} ->
          compare_value({operator, {:binding, rest_bindings}, right}, value, context)

        {{:error, _}, _} ->
          {operator, nil, right}
      end

    with {_, _, _} <- result do
      comparator.compare(result)
    end
  end

  defp get_value(nil, _, _) do
    {:error, :not_map}
  end

  defp get_value(value, {:variant, variant}, %Context{comparator: comparator}) do
    {:ok, comparator.apply_variant(value, variant)}
  end

  defp get_value(map, key, _) when is_map(map) do
    with :error <- Map.fetch(map, key),
         :error <- fetch_atom_key(map, key) do
      {:error, :key_missing}
    end
  end

  defp get_value(_, _, _), do: {:error, :not_map}

  defp fetch_atom_key(map, string) do
    key = String.to_existing_atom(string)
    Map.fetch(map, key)
  rescue
    _ ->
      :error
  end
end