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