lib/event_bus.ex

defmodule EventBus do
  @moduledoc """
  Traceable, extendable and minimalist event bus implementation for Elixir with
  built-in event store and event observation manager based on ETS.
  """

  alias EventBus.Manager.{
    Notification,
    Observation,
    Store,
    Subscription,
    Topic
  }

  alias EventBus.Model.Event

  @typedoc "EventBus.Model.Event struct"
  @type event :: Event.t()

  @typedoc "Event id"
  @type event_id :: String.t() | integer()

  @typedoc "Tuple of topic name and event id"
  @type event_shadow :: {topic(), event_id()}

  @typedoc "Event subscriber"
  @type subscriber :: subscriber_without_config() | subscriber_with_config()

  @typedoc "Subscriber configuration"
  @type subscriber_config :: any()

  @typedoc "List of event subscribers"
  @type subscribers :: list(subscriber())

  @typedoc "Event subscriber with config"
  @type subscriber_with_config :: {module(), subscriber_config()}

  @typedoc "Tuple of subscriber and event reference"
  @type subscriber_with_event_ref ::
          subscriber_with_event_shadow() | subscriber_with_topic_and_event_id()

  @typedoc "Tuple of subscriber and event shadow"
  @type subscriber_with_event_shadow :: {subscriber(), event_shadow()}

  @typedoc "Tuple of subscriber, topic and event id"
  @type subscriber_with_topic_and_event_id ::
          {subscriber(), topic(), event_id()}

  @typedoc "Tuple of subscriber and list of topic patterns"
  @type subscriber_with_topic_patterns :: {subscriber(), topic_patterns()}

  @typedoc "Event subscriber without config"
  @type subscriber_without_config :: module()

  @typedoc "Topic name"
  @type topic :: atom()

  @typedoc "List of topic names"
  @type topics :: list(topic())

  @typedoc "Regex pattern to match topic name"
  @type topic_pattern :: String.t()

  @typedoc "List of topic patterns"
  @type topic_patterns :: list(topic_pattern())

  @doc """
  Send an event to all subscribers.

  ## Examples

      event = %Event{id: 1, topic: :webhook_received,
        data: %{"message" => "Hi all!"}}
      EventBus.notify(event)
      :ok

  """
  @spec notify(event()) :: :ok
  defdelegate notify(event),
    to: Notification,
    as: :notify

  @doc """
  Check if a topic registered.

  ## Examples

      EventBus.topic_exist?(:demo_topic)
      true

  """
  @spec topic_exist?(topic()) :: boolean()
  defdelegate topic_exist?(topic),
    to: Topic,
    as: :exist?

  @doc """
  List all the registered topics.

  ## Examples

      EventBus.topics()
      [:metrics_summed]

  """
  @spec topics() :: topics()
  defdelegate topics,
    to: Topic,
    as: :all

  @doc """
  Register a topic.

  ## Examples

      EventBus.register_topic(:demo_topic)
      :ok

  """
  @spec register_topic(topic()) :: :ok
  defdelegate register_topic(topic),
    to: Topic,
    as: :register

  @doc """
  Unregister a topic.

  ## Examples

      EventBus.unregister_topic(:demo_topic)
      :ok

  """
  @spec unregister_topic(topic()) :: :ok
  defdelegate unregister_topic(topic),
    to: Topic,
    as: :unregister

  @doc """
  Subscribe a subscriber to the event bus.

  ## Examples

      EventBus.subscribe({MyEventSubscriber, [".*"]})
      :ok

      # For configurable subscribers you can pass tuple of subscriber and config
      my_config = %{}
      EventBus.subscribe({{OtherSubscriber, my_config}, [".*"]})
      :ok

  """
  @spec subscribe(subscriber_with_topic_patterns()) :: :ok
  defdelegate subscribe(subscriber_with_topic_patterns),
    to: Subscription,
    as: :subscribe

  @doc """
  Unsubscribe a subscriber from the event bus.

  ## Examples

      EventBus.unsubscribe(MyEventSubscriber)
      :ok

      # For configurable subscribers you must pass tuple of subscriber and config
      my_config = %{}
      EventBus.unsubscribe({OtherSubscriber, my_config})
      :ok

  """
  @spec unsubscribe(subscriber()) :: :ok
  defdelegate unsubscribe(subscriber),
    to: Subscription,
    as: :unsubscribe

  @doc """
  Check if the given subscriber subscribed to the event bus for the given topic
  patterns.

  ## Examples

      EventBus.subscribe({MyEventSubscriber, [".*"]})
      :ok

      EventBus.subscribed?({MyEventSubscriber, [".*"]})
      true

      EventBus.subscribed?({MyEventSubscriber, ["some_initialized"]})
      false

      EventBus.subscribed?({AnothEventSubscriber, [".*"]})
      false

  """
  @spec subscribed?(subscriber_with_topic_patterns()) :: boolean()
  defdelegate subscribed?(subscriber_with_topic_patterns),
    to: Subscription,
    as: :subscribed?

  @doc """
  List the subscribers.

  ## Examples

      EventBus.subscribers()
      [MyEventSubscriber]

      # One usual and one configured subscriber with its config
      EventBus.subscribers()
      [MyEventSubscriber, {OtherSubscriber, %{}}]

  """
  @spec subscribers() :: subscribers()
  defdelegate subscribers,
    to: Subscription,
    as: :subscribers

  @doc """
  List the subscribers for the given topic.

  ## Examples

      EventBus.subscribers(:metrics_received)
      [MyEventSubscriber]

      # One usual and one configured subscriber with its config
      EventBus.subscribers(:metrics_received)
      [MyEventSubscriber, {OtherSubscriber, %{}}]

  """
  @spec subscribers(topic()) :: subscribers()
  defdelegate subscribers(topic),
    to: Subscription,
    as: :subscribers

  @doc """
  Fetch an event.

  ## Examples

      EventBus.fetch_event({:hello_received, "123"})
      %EventBus.Model.Model{}

  """
  @spec fetch_event(event_shadow()) :: event() | nil
  defdelegate fetch_event(event_shadow),
    to: Store,
    as: :fetch

  @doc """
  Fetch an event's data.

  ## Examples

      EventBus.fetch_event_data({:hello_received, "123"})

  """
  @spec fetch_event_data(event_shadow()) :: any()
  defdelegate fetch_event_data(event_shadow),
    to: Store,
    as: :fetch_data

  @doc """
  Mark the event as completed for the subscriber.

  ## Examples

      topic        = :hello_received
      event_id     = "124"
      event_shadow = {topic, event_id}

      # For regular subscribers
      EventBus.mark_as_completed({MyEventSubscriber, event_shadow})

      # For configurable subscribers you must pass tuple of subscriber and config
      my_config = %{}
      subscriber  = {OtherSubscriber, my_config}

      EventBus.mark_as_completed({subscriber, event_shadow})
      :ok

  """
  @spec mark_as_completed(subscriber_with_event_ref()) :: :ok
  defdelegate mark_as_completed(subscriber_with_event_ref),
    to: Observation,
    as: :mark_as_completed

  @doc """
  Mark the event as skipped for the subscriber.

  ## Examples

      EventBus.mark_as_skipped({MyEventSubscriber, {:unmatched_occurred, "124"}})

      # For configurable subscribers you must pass tuple of subscriber and config
      my_config = %{}
      subscriber  = {OtherSubscriber, my_config}
      EventBus.mark_as_skipped({subscriber, {:unmatched_occurred, "124"}})
      :ok

  """
  @spec mark_as_skipped(subscriber_with_event_ref()) :: :ok
  defdelegate mark_as_skipped(subscriber_with_event_ref),
    to: Observation,
    as: :mark_as_skipped
end