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