# 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}}
```
