lib/pg_rest/type_caster.ex

defmodule PgRest.TypeCaster do
  @moduledoc """
  Casts string values from URL parameters to proper Elixir/Ecto types
  based on schema introspection.
  """

  import PgRest.Utils, only: [safe_to_existing_atom: 1]

  @skip_cast_ops ~w(is_null is fts plfts phfts wfts match imatch cs cd ov sl sr nxr nxl adj)a

  @doc """
  Casts all filter values to their proper types based on the schema.
  Returns `{:ok, filters}` with cast values or `{:error, reason}` on cast failure.
  """
  @spec cast_filters([map()], module()) :: {:ok, [map()]} | {:error, term()}
  def cast_filters(filters, schema_module) when is_list(filters) do
    cast_filters_acc(filters, schema_module, [])
  end

  defp cast_filters_acc([], _schema_module, acc), do: {:ok, Enum.reverse(acc)}

  defp cast_filters_acc([filter | rest], schema_module, acc) do
    {:ok, cast_filter} = cast_filter(filter, schema_module)
    cast_filters_acc(rest, schema_module, [cast_filter | acc])
  end

  defp cast_filter(%{logic: logic, conditions: conditions} = filter, schema_module)
       when logic in [:and, :or] do
    {:ok, cast_conditions} = cast_filters(conditions, schema_module)
    {:ok, %{filter | conditions: cast_conditions}}
  end

  defp cast_filter(%{logic: :not, condition: condition} = filter, schema_module) do
    {:ok, cast_condition} = cast_filter(condition, schema_module)
    {:ok, %{filter | condition: cast_condition}}
  end

  defp cast_filter(%{operator: op} = filter, _schema_module) when op in @skip_cast_ops do
    {:ok, filter}
  end

  defp cast_filter(%{field: field, operator: op, value: value} = filter, schema_module) do
    field_atom = safe_to_existing_atom(field)
    field_type = get_field_type(schema_module, field_atom)

    {:ok, cast_value} = cast_value(field_type, op, value)
    {:ok, %{filter | value: cast_value}}
  end

  defp cast_value(nil, _op, value), do: {:ok, value}

  defp cast_value(field_type, :in, values) when is_list(values) do
    cast_list(field_type, values, [])
  end

  defp cast_value(field_type, _op, value) when is_binary(value) do
    case Ecto.Type.cast(field_type, value) do
      {:ok, cast} -> {:ok, cast}
      :error -> {:ok, value}
    end
  end

  defp cast_value(_field_type, _op, value), do: {:ok, value}

  defp cast_list(_field_type, [], acc), do: {:ok, Enum.reverse(acc)}

  defp cast_list(field_type, [val | rest], acc) do
    case Ecto.Type.cast(field_type, val) do
      {:ok, cast} -> cast_list(field_type, rest, [cast | acc])
      :error -> cast_list(field_type, rest, [val | acc])
    end
  end

  defp get_field_type(schema_module, field_atom) do
    schema_module.__schema__(:type, field_atom)
  rescue
    _ -> nil
  end
end