Current section

Files

Jump to
ash_dispatch lib config.ex
Raw

lib/config.ex

defmodule AshDispatch.Config do
  @moduledoc """
  Centralized configuration access for AshDispatch.

  This module provides a single source of truth for all configuration values,
  with consistent defaults and clear documentation of each option.

  ## Why This Exists

  Configuration lookups were scattered across 30+ files, each with potentially
  different default values. This module ensures:

  1. **Consistent defaults** - Each config key has one canonical default
  2. **Single source of truth** - Change defaults in one place
  3. **Discoverability** - All config options documented together
  4. **Type safety** - Functions return expected types

  ## Usage

      # Instead of:
      Application.get_env(:ash_dispatch, :user_module)

      # Use:
      AshDispatch.Config.user_module()

  ## Configuration

  All options are configured under the `:ash_dispatch` application:

      config :ash_dispatch,
        user_module: MyApp.Accounts.User,
        domains: [MyApp.Orders, MyApp.Tickets],
        otp_app: :my_app,
        # ... see individual functions for all options
  """

  # ============================================================================
  # Core Resources
  # ============================================================================

  @doc """
  The user module for recipient resolution and preferences.

  Used by:
  - `AshDispatch.Event.Helpers` for recipient resolution
  - `AshDispatch.Dispatcher` for user extraction from context
  - Counter calculations

  ## Example

      config :ash_dispatch,
        user_module: MyApp.Accounts.User
  """
  @spec user_module() :: module() | nil
  def user_module do
    Application.get_env(:ash_dispatch, :user_module)
  end

  @doc """
  The user resource module (alias for user_module for clarity in some contexts).
  """
  @spec user_resource() :: module() | nil
  def user_resource do
    Application.get_env(:ash_dispatch, :user_resource)
  end

  @doc """
  The domain containing the user resource.
  """
  @spec user_domain() :: module() | nil
  def user_domain do
    Application.get_env(:ash_dispatch, :user_domain)
  end

  @doc """
  The delivery receipt resource module.

  Must be configured - returns `nil` if not set.

  ## Example

      config :ash_dispatch,
        delivery_receipt_resource: MyApp.Deliveries.DeliveryReceipt
  """
  @spec delivery_receipt_resource() :: module() | nil
  def delivery_receipt_resource do
    Application.get_env(:ash_dispatch, :delivery_receipt_resource)
  end

  @doc """
  The notification resource module.

  Must be configured - returns `nil` if not set.

  ## Example

      config :ash_dispatch,
        notification_resource: MyApp.Notifications.Notification
  """
  @spec notification_resource() :: module() | nil
  def notification_resource do
    Application.get_env(:ash_dispatch, :notification_resource)
  end

  # ============================================================================
  # Domains & OTP App
  # ============================================================================

  @doc """
  List of Ash domains that have dispatch-enabled resources.

  Used for:
  - Event discovery via DSL introspection
  - Counter loading
  - SDK generation

  ## Example

      config :ash_dispatch,
        domains: [MyApp.Orders, MyApp.Tickets, MyApp.Requests]
  """
  @spec domains() :: [module()]
  def domains do
    Application.get_env(:ash_dispatch, :domains, [])
  end

  @doc """
  The OTP application name.

  Used for:
  - Template resolution
  - Event module discovery
  """
  @spec otp_app() :: atom() | nil
  def otp_app do
    Application.get_env(:ash_dispatch, :otp_app)
  end

  # ============================================================================
  # Localization
  # ============================================================================

  @doc """
  Default locale for template resolution and content.

  Used as the ultimate fallback when no locale is specified. The full locale
  resolution priority is:

  1. Channel-level `locale` (static, e.g., `locale: "sv"`)
  2. Channel-level `locale_from` (dynamic from record field)
  3. Event-level `locale_from` configuration
  4. Resource-level `locale_from` configuration
  5. Auto-detected common fields: `visitor_locale`, `locale`
  6. This config default

  Template resolution then tries locale-specific templates first
  (e.g., `email.sv.html.heex`), falling back to non-localized templates.

  Defaults to `"en"`.

  ## Example

      config :ash_dispatch,
        default_locale: "sv"

  ## See Also

  - `AshDispatch.Resource.Dsl` - Resource-level `locales` configuration
  - `AshDispatch.TemplateResolver` - Template fallback chain
  """
  @spec default_locale() :: String.t()
  def default_locale do
    Application.get_env(:ash_dispatch, :default_locale, "en")
  end

  @doc """
  Optional Gettext backend for translating content strings.

  When configured, all `content:` block strings (notification_title,
  notification_message, etc.) are run through Gettext before variable
  interpolation. The locale is resolved via the dispatch locale chain.

  The content string IS the gettext msgid. Translations are looked up
  in the "notifications" domain.

  ## Example

      config :ash_dispatch,
        gettext_backend: MyAppWeb.Gettext

  Then content strings like `notification_title: "Usage Alert"` will
  be translated via `Gettext.dgettext(MyAppWeb.Gettext, "notifications", "Usage Alert")`.
  """
  @spec gettext_backend() :: module() | nil
  def gettext_backend do
    Application.get_env(:ash_dispatch, :gettext_backend)
  end

  @doc """
  Default Gettext domain used to translate DSL content strings.

  When a channel's `:notification_title`, `:notification_message`,
  `:subject`, etc. is defined as a literal in `dispatch do ... end`,
  `Dispatcher.translate_content/2` looks up the string via
  `Gettext.dgettext(backend, gettext_domain(), msgid)` before variable
  interpolation runs.

  Defaults to `"notifications"` so existing projects keep working.
  Override per app:

      config :ash_dispatch, :gettext_domain, "default"

  Projects that already extract i18n strings to `default.po` (e.g.
  multi-locale Phoenix apps) can consolidate their DSL content
  alongside the rest of their translations.
  """
  @spec gettext_domain() :: String.t()
  def gettext_domain do
    Application.get_env(:ash_dispatch, :gettext_domain, "notifications")
  end

  # ============================================================================
  # Email Configuration
  # ============================================================================

  @doc """
  Default from email address.

  Used when no `from` is specified in event module or inline DSL.

  Defaults to `"noreply@example.com"`.
  """
  @spec default_from_email() :: String.t()
  def default_from_email do
    Application.get_env(:ash_dispatch, :default_from_email, "noreply@example.com")
  end

  @doc """
  Default from name (sender name displayed in email clients).

  Used when no `from` is specified in event module or inline DSL.

  Defaults to the OTP app name (titlecased) or "System" if not configured.

  ## Example

      config :ash_dispatch,
        default_from_name: "Fyndgrossisten"
  """
  @spec default_from_name() :: String.t()
  def default_from_name do
    Application.get_env(:ash_dispatch, :default_from_name) ||
      case otp_app() do
        nil -> "System"
        app -> app |> to_string() |> String.capitalize()
      end
  end

  @doc """
  The email backend module for sending emails.

  Should implement the email backend behaviour.

  ## Example

      config :ash_dispatch,
        email_backend: AshDispatch.EmailBackend.Swoosh
  """
  @spec email_backend() :: module() | nil
  def email_backend do
    Application.get_env(:ash_dispatch, :email_backend)
  end

  @doc """
  The Swoosh mailer module (when using Swoosh backend).
  """
  @spec swoosh_mailer() :: module() | nil
  def swoosh_mailer do
    Application.get_env(:ash_dispatch, :swoosh_mailer)
  end

  @doc """
  The SMS backend module — should implement `AshDispatch.SMSBackend`.
  Returns `nil` when no backend is configured, in which case the SMS
  transport marks receipts `:skipped`.

  ## Example

      config :ash_dispatch,
        sms_backend: MyApp.SMS
  """
  @spec sms_backend() :: module() | nil
  def sms_backend do
    Application.get_env(:ash_dispatch, :sms_backend)
  end

  @doc """
  The Web Push backend module — should implement
  `AshDispatch.PushBackend`. Returns `nil` when no backend is
  configured, in which case the push transport marks receipts
  `:skipped` rather than failing the event.

  ## Example

      config :ash_dispatch,
        push_backend: MyApp.Push
  """
  @spec push_backend() :: module() | nil
  def push_backend do
    Application.get_env(:ash_dispatch, :push_backend)
  end

  # ============================================================================
  # URL Building
  # ============================================================================

  @doc """
  The URL builder module for generating admin/source URLs.

  Should implement `admin_url/2`, `source_url/2`, and optionally `resource_label/1`.
  """
  @spec url_builder() :: module() | nil
  def url_builder do
    Application.get_env(:ash_dispatch, :url_builder)
  end

  @doc """
  The Phoenix endpoint module for URL generation.
  """
  @spec endpoint() :: module() | nil
  def endpoint do
    Application.get_env(:ash_dispatch, :endpoint)
  end

  @doc """
  Base URL for the application (fallback when endpoint not available).
  """
  @spec base_url() :: String.t() | nil
  def base_url do
    Application.get_env(:ash_dispatch, :base_url)
  end

  # ============================================================================
  # Recipients & Audiences
  # ============================================================================

  @doc """
  The recipient resolver module.

  When configured, this module is used for all recipient resolution,
  replacing the legacy `audiences` configuration.

  Should implement `AshDispatch.RecipientResolver` behaviour.

  ## Example

      config :ash_dispatch,
        recipient_resolver: MyApp.RecipientResolver
  """
  @spec recipient_resolver() :: module() | nil
  def recipient_resolver do
    Application.get_env(:ash_dispatch, :recipient_resolver)
  end

  @doc """
  Audience configuration for recipient resolution (legacy).

  Maps audience atoms to filter configurations.

  **Note:** If `recipient_resolver` is configured, this option is ignored.
  Prefer using `recipient_resolver` for new applications.

  ## Example

      config :ash_dispatch,
        audiences: [
          admin: [admin: true],
          user: :user,
          support: [role: :support]
        ]
  """
  @spec audiences() :: keyword()
  def audiences do
    Application.get_env(:ash_dispatch, :audiences, [])
  end

  @doc """
  Recipient field configuration for extracting email/name from user structs.

  ## Example

      config :ash_dispatch,
        recipient_fields: [
          email: [
            default: :email,
            audiences: [admin: :work_email]
          ],
          name: [
            default: :display_name
          ]
        ]
  """
  @spec recipient_fields() :: keyword()
  def recipient_fields do
    Application.get_env(:ash_dispatch, :recipient_fields, [])
  end

  # ============================================================================
  # User Preferences
  # ============================================================================

  @doc """
  The user preference checker module.

  Defaults to `AshDispatch.UserPreference.Default` which allows all notifications.
  """
  @spec user_preference() :: module()
  def user_preference do
    Application.get_env(:ash_dispatch, :user_preference, AshDispatch.UserPreference.Default)
  end

  @doc """
  The preference provider module (legacy, prefer user_preference).
  """
  @spec preference_provider() :: module() | nil
  def preference_provider do
    Application.get_env(:ash_dispatch, :preference_provider)
  end

  # ============================================================================
  # Integrations
  # ============================================================================

  @doc """
  The PubSub module for broadcasting notifications.

  ## Contract

  Must be an **endpoint-shaped** module exposing both:

  - `subscribe(topic)` — for server-side consumers (e.g.
    `Mosis.Dispatch.Subscriber` or any GenServer that reacts to
    AshDispatch broadcasts in-process)
  - `broadcast(topic, event, payload)` — used by
    `AshDispatch.Transports.Broadcast` to publish to user / admin
    channels and by `Transports.InApp` for new-notification pushes

  Phoenix endpoints generate both functions automatically — set this
  to your application's endpoint module:

      config :ash_dispatch, pubsub_module: MyAppWeb.Endpoint

  **Do NOT set this to a bare `Phoenix.PubSub` registered name** (e.g.
  `MyApp.PubSub`). Those expose `Phoenix.PubSub.subscribe/2` (arity 2)
  and `Phoenix.PubSub.broadcast/3` as functions on the `Phoenix.PubSub`
  module itself, not on the registered-name atom — and consumers that
  call `pubsub.subscribe(topic)` or `pubsub.broadcast(topic, …)`
  directly would fail with `UndefinedFunctionError`.
  """
  @spec pubsub_module() :: module() | nil
  def pubsub_module do
    Application.get_env(:ash_dispatch, :pubsub_module)
  end

  @doc """
  The Phoenix channel topic prefix for user channels.

  Defaults to `"user"` which creates topics like `"user:user_id"`.
  Configure to match your channel setup, e.g., `"inbox"` for `"inbox:user_id"`.

  ## Example

      config :ash_dispatch,
        channel_topic: "inbox"
  """
  @spec channel_topic() :: String.t()
  def channel_topic do
    Application.get_env(:ash_dispatch, :channel_topic, "user")
  end

  @doc """
  The PubSub topic for admin/firehose broadcasts.

  Used by the `:broadcast` transport when audience is `:admin`.

  ## Example

      config :ash_dispatch,
        admin_channel_topic: "admin:firehose"
  """
  @spec admin_channel_topic() :: String.t()
  def admin_channel_topic do
    Application.get_env(:ash_dispatch, :admin_channel_topic, "admin:firehose")
  end

  @doc """
  The counter broadcaster module.

  Should implement `broadcast/3` for counter updates.
  """
  @spec counter_broadcaster() :: module() | nil
  def counter_broadcaster do
    Application.get_env(:ash_dispatch, :counter_broadcaster)
  end

  @doc """
  The counter broadcast function (alternative to counter_broadcaster module).
  """
  @spec counter_broadcast_fn() :: function() | nil
  def counter_broadcast_fn do
    Application.get_env(:ash_dispatch, :counter_broadcast_fn)
  end

  @doc """
  The permission checker module for policy checks.
  """
  @spec permission_checker() :: module() | nil
  def permission_checker do
    Application.get_env(:ash_dispatch, :permission_checker)
  end

  # ============================================================================
  # Database
  # ============================================================================

  @doc """
  The Ecto repo module.

  Used for Oban job queries and other database operations.
  """
  @spec repo() :: module() | nil
  def repo do
    Application.get_env(:ash_dispatch, :repo)
  end

  # ============================================================================
  # Event Modules
  # ============================================================================

  @doc """
  List of explicitly registered event modules.

  These are discovered in addition to DSL-based events.
  """
  @spec event_modules() :: [module()]
  def event_modules do
    Application.get_env(:ash_dispatch, :event_modules, [])
  end

  # ============================================================================
  # Compilation & Development
  # ============================================================================

  @doc """
  Whether to skip actual email delivery.

  When `true`, emails will not be sent and delivery receipts will be marked
  as `:skipped` with reason "email delivery disabled". Useful for development
  to prevent sending real emails while still testing the full dispatch flow.

  Defaults to `false`.

  ## Example

      # config/dev.exs
      config :ash_dispatch,
        skip_email_delivery: true
  """
  @spec skip_email_delivery?() :: boolean()
  def skip_email_delivery? do
    Application.get_env(:ash_dispatch, :skip_email_delivery, false)
  end

  @doc """
  Whether to compile templates at build time.

  Defaults to `false`.
  """
  @spec compile_templates?() :: boolean()
  def compile_templates? do
    Application.get_env(:ash_dispatch, :compile_templates, false)
  end

  @doc """
  Custom format extensions for template resolution.
  """
  @spec format_extensions() :: map()
  def format_extensions do
    Application.get_env(:ash_dispatch, :format_extensions, %{})
  end

  @doc """
  Output path for generated SDK files.
  """
  @spec sdk_output_path() :: String.t() | nil
  def sdk_output_path do
    Application.get_env(:ash_dispatch, :sdk_output_path)
  end

  # ============================================================================
  # Action Authorization
  # ============================================================================

  @doc """
  Authorizer module for the `send_now` action on delivery receipts.

  The module should implement `authorize/1` which receives the actor and returns:
  - `:ok` if authorized
  - `{:error, message}` if not authorized

  When `nil` (default), `send_now` is allowed for any authenticated actor.
  System calls (no actor) are always allowed regardless of this setting.

  ## Example

      # Define authorizer module
      defmodule MyApp.SendNowAuthorizer do
        def authorize(nil), do: :ok  # System calls always allowed
        def authorize(%{super_admin: true}), do: :ok
        def authorize(_), do: {:error, "Only super admins can trigger send now"}
      end

      # Configure in config.exs
      config :ash_dispatch,
        send_now_authorizer: MyApp.SendNowAuthorizer
  """
  @spec send_now_authorizer() :: module() | nil
  def send_now_authorizer do
    Application.get_env(:ash_dispatch, :send_now_authorizer)
  end
end