# `ExGram.Router.Filter`
[🔗](https://github.com/rockneurotiko/ex_gram_router/blob/v0.1.0/lib/ex_gram/router/filter.ex#L1)

Behaviour for ExGram.Router filters.

A filter is a module that decides whether a particular scope or handler
should match a given update. Filters receive the parsed update info tuple
(the same first argument ExGram passes to `handle/2`), the full context,
and any opts provided in the `filter` declaration.

## Callbacks

### `call/3` (required)

Returns `true` if the filter passes (the scope should be considered) or
`false` if it fails (skip this scope).

### `scope_extra/2` (optional)

Called by the dispatcher **after** `call/3` returns `true`, only when the
filter implements this callback. Returns a map that is merged into
`context.extra` before recursing into child scopes. This lets a filter
enrich the context for its children without affecting sibling scopes.

Because Elixir data is immutable, enrichment is automatically isolated:
siblings receive the original context from their parent's caller, not the
enriched copy.

## Example – basic filter

    defmodule MyApp.Filters.AdminOnly do
      @behaviour ExGram.Router.Filter

      def call(_update_info, context, _opts) do
        {:ok, user} = ExGram.Dsl.extract_user(context)
        user.id in Application.fetch_env!(:my_app, :admin_ids)
      end
    end

    scope do
      filter MyApp.Filters.AdminOnly
      handle &MyBot.admin_panel/1
    end

## Example – filter with propagation via `scope_extra/2`

The built-in `:callback_query` filter implements `scope_extra/2` and
supports the `propagate: true` option. When a parent scope sets
`propagate: true`, child scopes see an accumulated prefix in
`context.extra.__exgram_router__.text_prefix` so they can match against
the suffix of the callback data:

    scope do
      filter :callback_query, prefix: "proj:", propagate: true

      scope do
        filter :callback_query, "change"   # matches "proj:change"
        handle &Handlers.change_project/1
      end
    end

# `context`

```elixir
@type context() :: ExGram.Cnt.t()
```

# `opts`

```elixir
@type opts() :: term()
```

# `update_info`

```elixir
@type update_info() :: tuple() | atom()
```

# `call`

```elixir
@callback call(update_info(), context(), opts()) :: boolean()
```

Determines whether the current update matches this filter.

Returns `true` if the filter passes (the handler should be considered),
or `false` if the filter fails (skip this scope/handler).

# `format_filter`
*optional* 

```elixir
@callback format_filter(opts()) :: String.t()
```

Formats this filter as a human-readable string for display in the routing tree.

Called by `mix ex_gram.router.tree` when rendering a scope's filter list.
The returned string is used directly in the tree output - it should include
the filter name and any relevant opts representation.

This callback is optional. Filters that do not implement it fall back to
the default generic formatting provided by the mix task.

## Example

    # For `filter :command, :start` the default output is:
    #   Command(:start)

    # A filter that formats itself:
    def format_filter(nil), do: "MyFilter"
    def format_filter(opts), do: "MyFilter(#{inspect(opts)})"

# `scope_extra`
*optional* 

```elixir
@callback scope_extra(context(), opts()) :: map()
```

Optionally enriches `context.extra` for child scopes after this filter passes.

Returns a map that will be merged (via `Map.merge/2`) into `context.extra`
before the dispatcher recurses into child scopes. Returning an empty map
`%{}` has no effect.

The `update_info` is intentionally **not** provided — enrichment should be
driven by opts and any accumulated state already present in `context.extra`.

This callback is optional. Filters that do not need to enrich context can
omit it entirely.

# `text_filter`

```elixir
@spec text_filter(String.t(), term(), context() | nil) :: boolean()
```

Matches `text` against `match`, optionally using accumulated prefix from `context`.

When `context.extra.__exgram_router__.text_prefix` is set (e.g., by a parent
scope's `:callback_query` filter with `propagate: true`), the accumulated
prefix is prepended to the match target before comparison. This allows child
scopes to express matches relative to the prefix already consumed by an ancestor.

## Match forms

- `%Regex{}` — regex match against the full text (prefix is **not** prepended for
  regex matches, since regexes are inherently absolute)
- `binary` — exact equality after prepending accumulated prefix
- `keyword list` — one of:
  - `prefix: string` — text starts with `(accumulated_prefix <> prefix)`
  - `suffix: string` — text ends with the suffix (accumulated prefix is not used)
  - `contains: string` — text contains the substring (accumulated prefix is not used)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
