# `Snodo.Extension`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.2.0/lib/snodo/extension.ex#L1)

Behaviour for explicitly installed, exact-versioned out-of-tree extensions.

Extension-owned routes deliberately cover top-level client-to-server
requests only. An optional `around_dispatch/4` callback can wrap core
protocol execution for an advertised, exact-version-compatible extension.
Its continuation accepts a request context, allowing middleware to pass a
derived immutable context to the next extension and ultimately the core
handler. Middleware also runs before peer negotiation succeeds, and can
inspect `Snodo.Context.extensions` to distinguish negotiated requests.

An optional `missing_capability_error/2` callback can customize the error for
an extension-owned method when the server advertised the extension but the
client did not. Extensions without the callback retain method-not-found
behavior.

An optional `transport_policy/2` callback can adapt the selected protocol's
transport policy for an extension-owned route. The callback is considered
only for an exact-version-compatible route advertised by the server;
installed-only extensions cannot affect transport admission.

Extensions may optionally contribute fields to `subscriptions/listen` with
`subscription_filter/2` and shape matching source events with
`shape_subscription_event/3`. The framework invokes these callbacks only for
an installed, advertised, exact-version-compatible extension and retains
per-extension filter ownership through the stream lifecycle.

Other outbound methods and MRTR-embedded operations require additional
routing infrastructure and cannot be advertised as extension-owned routes
through this behaviour yet.

# `continuation`

```elixir
@type continuation() :: (Snodo.Context.t() -&gt; dispatch_result())
```

# `dispatch_result`

```elixir
@type dispatch_result() :: {:ok, Snodo.Result.t()} | {:error, Snodo.Error.t()}
```

# `around_dispatch`
*optional* 

```elixir
@callback around_dispatch(
  operation :: term(),
  params :: map(),
  Snodo.Context.t(),
  continuation()
) ::
  dispatch_result()
```

# `dispatch`

```elixir
@callback dispatch(operation :: term(), params :: map(), Snodo.Context.t()) ::
  dispatch_result()
```

# `id`

```elixir
@callback id() :: String.t()
```

# `methods`

```elixir
@callback methods() :: [Snodo.Extension.Method.t()]
```

# `missing_capability_error`
*optional* 

```elixir
@callback missing_capability_error(method :: String.t(), Snodo.Context.t()) ::
  Snodo.Error.t() | :method_not_found
```

# `negotiate`

```elixir
@callback negotiate(client_settings :: map(), server_settings :: map()) ::
  {:ok, map()} | :not_negotiated | {:error, Snodo.Error.t()}
```

# `shape_error`

```elixir
@callback shape_error(Snodo.Error.t(), Snodo.Context.t()) :: map()
```

# `shape_result`

```elixir
@callback shape_result(operation :: term(), Snodo.Result.t(), Snodo.Context.t()) :: map()
```

# `shape_subscription_event`
*optional* 

```elixir
@callback shape_subscription_event(
  Snodo.Subscription.Event.t(),
  Snodo.Envelope.id(),
  Snodo.Context.t()
) :: map()
```

# `subscription_filter`
*optional* 

```elixir
@callback subscription_filter(requested_filter :: map(), Snodo.Context.t()) ::
  {:ok, accepted_filter :: map()} | {:error, Snodo.Error.t()}
```

# `transport_policy`
*optional* 

```elixir
@callback transport_policy(Snodo.Envelope.t(), Snodo.Transport.Policy.t()) ::
  Snodo.Transport.Policy.t()
```

# `validate_operation`

```elixir
@callback validate_operation(operation :: term(), params :: map(), Snodo.Context.t()) ::
  :ok | {:error, Snodo.Error.t()}
```

---

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