Packages

Event-driven notification system for Ash Framework with multiple transport types

Current section

Files

Jump to
ash_dispatch lib context.ex
Raw

lib/context.ex

defmodule AshDispatch.Context do
@moduledoc """
Event context passed to all event callbacks and transports.
The context contains:
- Event data (resources, user, etc.)
- Metadata (event_id, source, timestamp)
- Configuration (locale, base_url)
- Computed values from enrichment
## Example
%Context{
event_id: "orders.created",
data: %{
order: %ProductOrder{...},
user: %User{...}
},
user: %User{...},
locale: "sv",
base_url: "https://myapp.com",
now: ~U[2025-01-15 10:00:00Z],
resource_key: :order
}
"""
@type t :: %__MODULE__{
event_id: String.t(),
data: map(),
variables: map(),
resource_key: atom() | nil,
priority: :urgent | :standard | :informational,
locale: String.t(),
user: struct() | nil,
source: String.t() | nil,
base_url: String.t(),
now: DateTime.t(),
metadata: map(),
opts: map()
}
alias AshDispatch.Config
defstruct [
:event_id,
:data,
:resource_key,
:user,
:source,
:locale,
priority: :standard,
base_url: "",
now: nil,
metadata: %{},
opts: %{},
variables: %{}
]
@doc """
Creates a new context.
## Options
- `:event_id` - Event identifier
- `:data` - Event data map (resources, domain objects)
- `:variables` - Variables map (tokens, simple values, template data)
- `:resource_key` - Hint for nested user lookup
- `:locale` - Locale for i18n
- `:user` - Actor/user triggering the event
- `:base_url` - Base URL for link building
- `:metadata` - Additional metadata
- `:opts` - Additional options
## Example
Context.new(
event_id: "orders.created",
data: %{order: order, user: user},
variables: %{confirmation_token: token},
user: user,
locale: "sv"
)
"""
def new(opts) do
%__MODULE__{
event_id: Keyword.fetch!(opts, :event_id),
data: atomize_keys(Keyword.get(opts, :data, %{})),
variables: atomize_keys(Keyword.get(opts, :variables, %{})),
resource_key: Keyword.get(opts, :resource_key),
locale: Keyword.get(opts, :locale, Config.default_locale()),
user: Keyword.get(opts, :user),
source: Keyword.get(opts, :source),
base_url: Keyword.get(opts, :base_url, get_default_base_url()),
now: Keyword.get(opts, :now, DateTime.utc_now()),
metadata: Keyword.get(opts, :metadata, %{}),
opts: Map.new(opts)
}
end
@doc """
Recursively atomizes string keys in maps.
Skips structs to preserve Ash resources.
## Examples
iex> Context.atomize_keys(%{"user" => %{"email" => "test@example.com"}})
%{user: %{email: "test@example.com"}}
iex> Context.atomize_keys(%User{email: "test@example.com"})
%User{email: "test@example.com"}
"""
def atomize_keys(%{__struct__: _} = struct), do: struct
def atomize_keys(map) when is_map(map) do
Map.new(map, fn {k, v} ->
key = if is_binary(k), do: String.to_atom(k), else: k
{key, atomize_keys(v)}
end)
end
def atomize_keys(list) when is_list(list), do: Enum.map(list, &atomize_keys/1)
def atomize_keys(value), do: value
@doc """
Enriches context with additional data.
Used by Dispatcher to auto-populate common fields like admins.
## Example
context
|> Context.enrich(:admins, list_of_admins)
|> Context.enrich(:metadata, %{custom: "value"})
"""
def enrich(%__MODULE__{} = context, key, value) when is_atom(key) do
%{context | data: Map.put(context.data, key, value)}
end
def enrich(%__MODULE__{} = context, map) when is_map(map) do
%{context | data: Map.merge(context.data, atomize_keys(map))}
end
@doc """
Merges metadata into context.
"""
def merge_metadata(%__MODULE__{} = context, metadata) when is_map(metadata) do
%{context | metadata: Map.merge(context.metadata, metadata)}
end
@doc """
Gets all assigns for template rendering by merging data and variables.
Variables take precedence over data in case of key conflicts.
## Example
context = %Context{
data: %{user: user, order: order},
variables: %{confirmation_token: "abc123"}
}
Context.template_assigns(context)
# => %{user: user, order: order, confirmation_token: "abc123"}
"""
def template_assigns(%__MODULE__{} = context) do
Map.merge(context.data, context.variables)
end
# Private helpers
defp get_default_base_url do
# Priority order:
# 1. Configured endpoint module (calls Endpoint.url())
# 2. PHX_HOST environment variable
# 3. Explicit base_url config (deprecated)
# 4. Fallback to localhost
cond do
endpoint = Config.endpoint() ->
endpoint.url()
host = System.get_env("PHX_HOST") ->
scheme = System.get_env("PHX_SCHEME", "https")
port = System.get_env("PHX_PORT", "443")
case {scheme, port} do
{"https", "443"} -> "#{scheme}://#{host}"
{"http", "80"} -> "#{scheme}://#{host}"
_ -> "#{scheme}://#{host}:#{port}"
end
base_url = Config.base_url() ->
base_url
true ->
"http://localhost:4000"
end
end
end