lib/seeker/query.ex

defmodule Seeker.Query do
  @moduledoc """
  Query implementation for `Seeker`.
  """

  alias Seeker.Query.Predicates
  alias Seeker.PredicateNotFoundError

  @predicates [
    :not_eq,
    :eq,
    :not_in,
    :in,
    :not_i_cont,
    :i_cont,
    :not_cont,
    :cont,
    :not_start,
    :start,
    :not_end,
    :end,
    :gt,
    :gteq,
    :lt,
    :lteq
  ]

  def call(scope, filters) do
    Enum.reduce(filters, scope, fn {key, value}, scope ->
      Predicates.call(scope, extract_data_from_key(key), value)
    end)
  end

  defp extract_data_from_key(key) do
    with {:ok, key} <- sanitize_key(key),
         {:ok, predicate} <- extract_predicate(key),
         {:ok, subject} <- extract_subject(key, predicate) do
      {String.to_existing_atom(subject), predicate}
    else
      {:error, :predicate_not_found} ->
        raise PredicateNotFoundError, key: key
    end
  end

  defp sanitize_key(key) do
    {:ok, to_string(key)}
  end

  defp extract_predicate(key) do
    case Enum.find(@predicates, &String.ends_with?(key, "_#{&1}")) do
      nil -> {:error, :predicate_not_found}
      predicate -> {:ok, predicate}
    end
  end

  defp extract_subject(key, predicate) do
    {:ok, String.replace_suffix(key, "_#{predicate}", "")}
  end
end