Skip to main content

lib/counterpoint/envelope.ex

defmodule Counterpoint.Envelope do
  @moduledoc """
  A deserialized event together with its store metadata.

  Projections and commands receive `Envelope` structs when reading events.
  Pattern-match on the `data` field to handle specific event types:

      def apply(state, %Counterpoint.Envelope{data: %MyApp.Events.OrderPlaced{order_id: id}}) do
        Map.put(state, :order_id, id)
      end

  ## Fields

  - `type` – the event's type string (e.g. `"OrderPlaced"`).
  - `tags` – tags stored alongside the event.
  - `data` – the deserialized event struct.
  - `position` – opaque store position used for optimistic-concurrency conditions.
  - `occurred_at` – parsed from the event map's `"occurred_at"` key, if present.
  """

  defstruct [:type, :tags, :data, :position, :occurred_at]

  @type t :: %__MODULE__{
          type: String.t(),
          tags: [String.t()],
          data: struct(),
          position: binary(),
          occurred_at: DateTime.t() | nil
        }

  @doc false
  def deserialize(%{type_name: type_string, tags: tags, data: data_json, position: position}) do
    with {:ok, mod} <- Counterpoint.EventRegistry.lookup(type_string),
         {:ok, raw_map} <- Jason.decode(data_json),
         data <- mod.from_map(raw_map),
         occurred_at <- parse_occurred_at(raw_map) do
      {:ok,
       %__MODULE__{
         type: type_string,
         tags: tags,
         data: data,
         position: position,
         occurred_at: occurred_at
       }}
    end
  end

  @doc false
  def deserialize_many(raw_events) do
    Enum.flat_map(raw_events, fn raw ->
      case deserialize(raw) do
        {:ok, env} -> [env]
        {:error, _} -> []
      end
    end)
  end

  @doc false
  def serialize(%__MODULE__{data: data, type: type, tags: tags}) do
    mod = data.__struct__
    %{type_name: type, tags: tags, data: Jason.encode!(mod.to_map(data))}
  end

  defp parse_occurred_at(%{"occurred_at" => iso}) when is_binary(iso) do
    case DateTime.from_iso8601(iso) do
      {:ok, dt, _} -> dt
      _ -> nil
    end
  end

  defp parse_occurred_at(_), do: nil
end