Defines reusable filters that compile to Ecto dynamic expressions.
use Fltr turns a module into a filter parser and compiler. The module
declares the filter names it accepts, validates untrusted input with the
generated parse/1, and converts validated filters with the generated
to_expr/1.
Defining filters
Declare each filter with the number of arguments it accepts, then implement a
matching to_expr clause:
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 declared argument count does not include the filter name. In the example,
:active requires to_expr/1, while :id and :name require to_expr/2.
Fltr checks that every required callback arity exists when the module compiles.
use Fltr also imports Ecto.Query.dynamic/2 into the filter module.
Parsing external input
String-named lists and tuples are external input. parse/1 checks their names
and argument counts, converts names to the declared atoms without creating new
atoms, and returns a canonical tuple:
TeamFilter.parse(["id", 7])
#=> {:ok, {:id, 7}}Arguments pass through unchanged by default. Define parse/2 clauses when a
filter needs validation or normalization:
def parse(:id, id) when is_binary(id) do
case Integer.parse(id) do
{id, ""} when id > 0 -> {:ok, id}
_other -> :error
end
end
def parse(:id, _id), do: :errorFor a one-argument filter, parse/2 receives and returns the argument directly.
For a filter with two or more arguments, it receives a tuple and must return a
tuple of the declared size. Filters with no arguments do not invoke parse/2.
Parsed leaves preserve the declared shape: {:active}, {:id, id}, or
{:score_between, minimum, maximum}.
Atom-named tuples are considered canonical expressions. Fltr checks their name
and argument count, but deliberately does not pass their values through
parse/2. Always call parse/1 on external input before converting it into an
atom-named tuple.
Boolean groups
:all joins child expressions with and; :any joins them with or. Groups
require at least one child and may be nested:
{:any, [{:id, id}, {:active}]}
{:all, [{:name, name}, {:active}]}External groups use the equivalent list form with string names:
["any", [["id", id], ["active"]]]Building a query
Parse external input, compile the canonical filter, and interpolate the resulting dynamic expression into an Ecto query:
import Ecto.Query
{:ok, filter} = TeamFilter.parse(["name", "Rovers"])
filter = TeamFilter.to_expr(filter)
Team
|> where(^filter)
|> Repo.all()