Packages
Event-driven notification system for Ash Framework with multiple transport types
Current section
Files
Jump to
Current section
Files
lib/helpers/event_registry.ex
defmodule AshDispatch.EventRegistry do
@moduledoc """
Auto-discovery registry for event modules.
This module eliminates the need for manual `event_modules` configuration by
automatically discovering events from Ash domains at runtime.
## How It Works
1. Scans all configured Ash domains for the given `otp_app`
2. Finds resources with `AshDispatch.Resource` extension
3. Reads event definitions from the `dispatch` DSL section
4. Returns event information including any associated modules
## Usage
Instead of manually configuring:
# OLD - Manual config (no longer needed!)
config :ash_dispatch, :event_modules, [
{"order.created", MyApp.Events.OrderCreated},
{"ticket.assigned", MyApp.Events.TicketAssigned}
]
Events are now auto-discovered:
# Get all event modules (for backward compatibility)
EventRegistry.get_event_modules()
# Find a specific event
{:ok, event} = EventRegistry.find_event("order.created")
# Find module for an event
{:ok, module} = EventRegistry.find_module("order.created")
## Configuration
The registry uses the `:otp_app` config to know which app's domains to scan:
config :ash_dispatch, :otp_app, :my_app
And the app must have domains configured:
config :my_app, :ash_domains, [MyApp.Orders, MyApp.Tickets]
"""
alias AshDispatch.Config
alias AshDispatch.Naming
require Logger
@doc """
Get all event modules in the format `[{event_id, module}, ...]`.
This provides backward compatibility with the old `:event_modules` config format.
Only returns events that have an associated module (explicit or generated).
Uses the configured `:otp_app` to determine which domains to scan.
"""
@spec get_event_modules() :: [{String.t(), module()}]
def get_event_modules do
otp_app = Config.otp_app()
if otp_app do
event_modules(otp_app)
else
# Fall back to legacy config for backward compatibility
Config.event_modules()
end
end
@doc """
Get all event modules for a specific otp_app.
Returns a list of `{event_id, module}` tuples for events that have modules.
Events without modules (pure inline DSL) are excluded.
"""
@spec event_modules(atom()) :: [{String.t(), module()}]
def event_modules(otp_app) do
otp_app
|> all_events()
|> Enum.filter(fn event -> event.module != nil end)
|> Enum.map(fn event -> {event.event_id, event.module} end)
end
@doc """
Find an event by its event_id.
Returns `{:ok, event_info}` or `{:error, :not_found}`.
"""
@spec find_event(String.t(), atom() | nil) :: {:ok, map()} | {:error, :not_found}
def find_event(event_id, otp_app \\ nil) do
otp_app = otp_app || Config.otp_app()
if otp_app do
case Enum.find(all_events(otp_app), &(&1.event_id == event_id)) do
nil -> {:error, :not_found}
event -> {:ok, event}
end
else
{:error, :not_found}
end
end
@doc """
Find the module for an event by its event_id.
Returns `{:ok, module}`, `{:error, :no_module}` if event exists but has no module,
or `{:error, :not_found}` if event doesn't exist.
"""
@spec find_module(String.t(), atom() | nil) ::
{:ok, module()} | {:error, :not_found | :no_module}
def find_module(event_id, otp_app \\ nil) do
case find_event(event_id, otp_app) do
{:ok, %{module: nil}} -> {:error, :no_module}
{:ok, %{module: module}} -> {:ok, module}
{:error, reason} -> {:error, reason}
end
end
@doc """
Get all events from all resources in the configured domains.
Returns a list of event info maps with:
- `:event_id` - The event identifier (e.g., "order.created")
- `:name` - The event name atom (e.g., :created)
- `:module` - The event module (if any)
- `:resource` - The resource module that defines this event
- `:channels` - Channel configurations from DSL
- `:content` - Content configuration from DSL
- `:metadata` - Metadata configuration from DSL
"""
@spec all_events(atom()) :: [map()]
def all_events(otp_app) do
domains = get_domains(otp_app)
domains
|> Enum.flat_map(&get_domain_resources/1)
|> Enum.filter(&uses_ash_dispatch?/1)
|> Enum.flat_map(&extract_events/1)
end
# Private helpers
defp get_domains(otp_app) do
# Priority:
# 1. Standard Ash :ash_domains config (uses same config as Ash)
# 2. Fallback to :ash_dispatch :domains config (for explicit override)
case Application.get_env(otp_app, :ash_domains, []) do
[] -> Config.domains()
domains -> domains
end
end
defp get_domain_resources(domain) do
Ash.Domain.Info.resources(domain)
rescue
_ -> []
end
defp uses_ash_dispatch?(resource) do
AshDispatch.Resource in Spark.extensions(resource)
rescue
_ -> false
end
defp extract_events(resource) do
# Get events from the dispatch section
events = Spark.Dsl.Extension.get_entities(resource, [:dispatch])
events
|> Enum.filter(&is_event_entity?/1)
|> Enum.map(fn event ->
event_id = event.event_id || Naming.event_id(resource, event.name)
%{
event_id: event_id,
name: event.name,
module: resolve_module(event, resource),
resource: resource,
channels: event.channels,
content: event.content,
metadata: event.metadata,
trigger_on: event.trigger_on
}
end)
rescue
error ->
Logger.warning(
"[EventRegistry] Failed to extract events from #{inspect(resource)}: #{inspect(error)}"
)
[]
end
# Check if entity is an event (not AudiencePrefix, AudienceOverride, etc.)
defp is_event_entity?(%{__struct__: struct}) do
struct == AshDispatch.Resource.Dsl.Event
end
defp is_event_entity?(_), do: false
# Resolve module - explicit module from DSL takes precedence
defp resolve_module(event, resource) do
case event.module do
module when not is_nil(module) ->
# Explicit module in DSL
module
nil ->
# Try to find auto-generated module using Naming convention
domain_name = Naming.domain_name(resource)
derived_module = Naming.event_module(resource, domain_name, event.name)
if Code.ensure_loaded?(derived_module) do
derived_module
else
nil
end
end
end
end