Skip to main content

lib/ping_pong.ex

defmodule PingPong do
  @moduledoc """
  Public API and OTP application entry point for PingPong.

  PingPong provides a small, consistent interface for sending notifications to
  registered services. The built-in services are `:discord`, `:telegram`, and
  `:mock`; additional services can be registered through application
  configuration.

  ## Results

  Synchronous calls return either `{:ok, response}` or an error tuple. Unknown
  services return `{:error, {:unknown_service, service}}`.

  Asynchronous calls return `{:ok, task}` when a service is found. Awaiting the
  task returns the service result:

      {:ok, task} = PingPong.send_async(:mock, %{message: "Ping!"}, %{})
      Task.await(task)

  """

  use Application

  @typedoc "Notification identifier used by `send_multiple/1`."
  @type id :: atom

  @typedoc "Registered service key, such as `:discord`, `:telegram`, or `:mock`."
  @type service :: atom

  @typedoc "Service-specific notification payload."
  @type payload :: map

  @typedoc "Service-specific delivery options."
  @type options :: map

  @typedoc "Standard result returned by PingPong services."
  @type result :: {:ok, any} | {:error, {atom, any}}

  @typedoc "Delivery mode used by `PingPong.Notification`."
  @type send_type :: :sync | :async

  @typedoc "A notification tuple accepted by `send_multiple/1`."
  @type config :: {service, payload, options}

  @doc false
  def start(_start_type, _start_args) do
    Task.Supervisor.start_link(name: PingPong.Supervisor, max_restarts: 2)
  end

  @doc """
  Sends a notification through a registered service.

  The `payload` and `options` maps are validated by the selected service.

  ## Examples

      iex> PingPong.send(:mock, %{message: "Ping!"}, %{})
      {:ok, "Pong! 🏓"}

      iex> PingPong.send(:unknown, %{}, %{})
      {:error, {:unknown_service, :unknown}}

  """
  @spec send(service(), payload(), options()) :: result()
  defdelegate send(service, payload, options), to: PingPong.Dispatcher, as: :dispatch

  @doc """
  Sends a notification in a supervised task.

  Returns `{:ok, task}` when the service exists. Use `Task.await/1` to receive
  the service result. Unknown services return an error immediately.

  ## Examples

      {:ok, task} = PingPong.send_async(:mock, %{message: "Ping!"}, %{})
      {:ok, "Pong! 🏓"} = Task.await(task)

  """
  @spec send_async(service(), payload(), options()) :: result()
  defdelegate send_async(service, payload, options), to: PingPong.Dispatcher, as: :dispatch_async

  @doc """
  Sends multiple named notifications synchronously.

  Accepts an enumerable of `{id, notification}` pairs, where each notification
  is a `{service, payload, options}` tuple. The return value preserves each
  notification ID alongside its result.

  ## Examples

      notifications = [
        first: {:mock, %{message: "Ping!"}, %{}},
        second: {:mock, %{}, %{}}
      ]

      PingPong.send_multiple(notifications)

  """
  @spec send_multiple(Enumerable.t({id(), config()})) :: [{id(), result()}]
  def send_multiple(notifications) do
    Enum.map(notifications, fn {id, notification} ->
      {id, PingPong.Notification.send_notification(notification, :sync)}
    end)
  end
end