Packages

Composable, reusable filters for Ecto queries with optional external input parsing.

Current section

Files

Jump to
fltr README.md
Raw

README.md

# Fltr
Fltr lets you define and compose reusable filters for Ecto queries, with
optional parsing for external input.
## Usage
Define the supported filters and their argument counts, then implement one
`to_expr` clause for each filter:
```elixir
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)
end
```
The 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.
### A single filter
Pass a canonical filter directly to `to_expr/1`, then interpolate the resulting
dynamic expression into an Ecto query:
```elixir
import Ecto.Query
filter = TeamFilter.to_expr({:active})
Team
|> where(^filter)
|> Repo.all()
```
### Boolean groups
Use `:all` to require every child filter and `:any` to require at least one.
Groups can contain other groups:
```elixir
filter =
{:all,
[
{:active},
{:any, [{:id, 7}, {:name, "Rovers"}]}
]}
filter = TeamFilter.to_expr(filter)
Team
|> where(^filter)
|> Repo.all()
```
### External filters
When a filter comes from outside the application, pass it through the
`parse/1` function generated by `use Fltr` before compiling it:
```elixir
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
end
```
External filters use strings and lists:
```elixir
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:
```elixir
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: :error
```
Lists 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:
```elixir
{:error, {:invalid_filter, input}}
{:error, {:unknown_filter, name}}
{:error, {:invalid_arguments, name, arguments}}
```