lib/polyn/event.ex

defmodule Polyn.Event do
  @moduledoc """
  The Event structure used throughout Polyn.
  """

  alias Polyn.Naming

  defstruct id: nil,
            specversion: nil,
            type: nil,
            data: nil,
            dataschema: nil,
            datacontenttype: nil,
            source: nil,
            time: nil,
            polyndata: %{
              clientlang: "elixir",
              clientlangversion: System.build_info().version
            }

  @typedoc """
  The Event structure used throughout Polyn.

  * `id` - Identifies the event.
  * `specversion` - The version of the CloudEvents specification which the event uses.
  * `type` - Describes the type of event related to the originating occurrence.
  * `data` - The event payload.
  * `dataschema` - Identifies the schema that data adheres to.
  * `datacontenttype` - Content type of the data value. Must adhere to RFC 2046 format.
  * `source` - Identifies the context in which an event happened.
  * `time` - Timestamp of when the occurrence happened. Must adhere to RFC 3339.
  * `polyndata` - Information about the client that produced the event and additional data
  """
  @type t() :: %__MODULE__{
          id: String.t(),
          specversion: String.t(),
          type: String.t(),
          data: any(),
          dataschema: String.t(),
          datacontenttype: String.t(),
          source: String.t(),
          time: String.t(),
          polyndata: map()
        }

  @doc """
  Create a new `Polyn.Event`
  """
  @spec new(fields :: keyword()) :: t()
  def new(fields) when is_list(fields) do
    fields =
      Keyword.put_new(fields, :id, new_event_id())
      |> Keyword.put_new(:time, new_timestamp())
      |> Keyword.put_new(:source, full_source())

    struct!(__MODULE__, fields)
    |> add_polyn_version()
  end

  @spec new(fields :: map()) :: t()
  def new(fields) when is_map(fields) do
    Enum.into(fields, Keyword.new()) |> new()
  end

  @doc """
  Generate a new event id
  """
  def new_event_id do
    UUID.uuid4()
  end

  @doc """
  Generate a new timestamp for the event
  """
  def new_timestamp do
    DateTime.to_iso8601(DateTime.utc_now())
  end

  # we don't need to add both the atom and string versions
  defp add_polyn_version(%__MODULE__{polyndata: %{"clientversion" => _version}} = event) do
    event
  end

  defp add_polyn_version(%__MODULE__{} = event) do
    put_in(event, [Access.key!(:polyndata), :clientversion], polyn_version())
  end

  defp polyn_version do
    # Interporalating cuz `vsn` comes out as charlist instead of String
    "#{Application.spec(:polyn, :vsn)}"
  end

  @doc """
  Get the Event `source` prefixed with reverse domain name
  """
  @spec full_source(source :: binary() | nil) :: binary()
  @spec full_source() :: binary()
  def full_source(nil), do: full_source()

  def full_source(source) do
    Naming.validate_source_name!(source)
    source = String.replace(source, ~r/#{full_source()}{1}:?/, "")

    case source do
      "" -> full_source()
      name -> "#{full_source()}:#{Naming.dot_to_colon(name)}"
    end
  end

  def full_source do
    Naming.validate_source_name!(source_root())
    Naming.dot_to_colon("#{domain()}:#{source_root()}")
  end

  @doc """
  Get the Event `type` prefixed with reverse domain name
  """
  @spec full_type(type :: binary()) :: binary()
  def full_type(type) do
    Naming.validate_message_name!(type)
    "#{domain()}.#{Naming.trim_domain_prefix(type)}"
  end

  # The `domain` that all events will happen under
  defp domain do
    Application.fetch_env!(:polyn, :domain)
  end

  defp source_root do
    Application.fetch_env!(:polyn, :source_root)
  end
end