Fltr lets you define and compose reusable filters for Ecto queries, with optional parsing for external input.
Define the supported filters and their argument counts, then implement one
to_expr clause for each filter:
defmodule TeamFilter do
use Fltr, filters: [active: 0, id: 1, name: 1]
def to_expr(:active), do: dynamic([team], team.active)
def to_expr(:id, id), do: dynamic([team], team.id == ^id)
def to_expr(:name, name), do: dynamic([team], team.name == ^name)
endThe argument count does not include the filter name. Here, :active maps to
to_expr/1, while :id and :name map to to_expr/2. Fltr verifies the
required callback arities when the module compiles.
Pass a canonical filter directly to to_expr/1, then interpolate the resulting
dynamic expression into an Ecto query:
import Ecto.Query
filter = TeamFilter.to_expr({:active})
Team
|> where(^filter)
|> Repo.all()Use :all to require every child filter and :any to require at least one.
Groups can contain other groups:
filter =
{:all,
[
{:active},
{:any, [{:id, 7}, {:name, "Rovers"}]}
]}
filter = TeamFilter.to_expr(filter)
Team
|> where(^filter)
|> Repo.all()When a filter comes from outside the application, pass it through the
parse/1 function generated by use Fltr before compiling it:
import Ecto.Query
def list_teams(filter) do
with {:ok, filter} <- TeamFilter.parse(filter) do
filter = TeamFilter.to_expr(filter)
Team
|> where(^filter)
|> Repo.all()
end
endExternal filters use strings and lists:
list_teams([
"all",
[
["active"],
["any", [["id", 7], ["name", "Rovers"]]]
]
])Arguments pass through unchanged by default. Define parse/2 in the filter
module when an argument needs validation or normalization:
def parse(:id, id) when is_binary(id) do
case Integer.parse(id) do
{id, ""} -> {:ok, id}
_other -> :error
end
end
def parse(:id, _id), do: :errorLists and string-named tuples are treated as external input and invoke
parse/2. Atom-named tuples are trusted canonical expressions: Fltr checks
their argument count but does not parse their values again. Always call
parse/1 before converting external input into an atom-named tuple.
parse/1 returns an error describing invalid input:
{:error, {:invalid_filter, input}}
{:error, {:unknown_filter, name}}
{:error, {:invalid_arguments, name, arguments}}