lib/events/event.ex
defmodule ElixirTools.Events.Event do
@moduledoc """
A structure for working with events.
`event_id_seed` is used for `event_id` generation(UUID5) together with `name` & `version`.
So if all values are the same event will be updated.
"""
alias ExJsonSchema.Validator
alias __MODULE__
@type t :: %Event{
name: String.t(),
version: String.t(),
payload: map,
event_id_seed: Ecto.UUID.t(),
event_id_seed_optional: String.t(),
occurred_at: DateTime.t() | nil
}
@typep event_schema :: map
@typep return :: :ok | {:error, reason :: String.t()}
@enforce_keys ~w(name event_id_seed)a
defstruct [
:name,
:event_id_seed,
:occurred_at,
payload: %{},
version: "1.0.0",
event_id_seed_optional: ""
]
@typep publish_opts :: {:adapter, module} | {:uuid_module, module} | {:timex_module, module}
@spec publish_deprecated(Event.t(), [publish_opts]) :: return
def publish_deprecated(event, opts \\ []) do
adapter =
opts[:adapter] || Application.get_env(:pagantis_elixir_tools, ElixirTools.Events)[:adapter]
with :ok <- validate(event) do
try do
event
|> add_envelope(opts)
|> adapter.publish()
rescue
e -> {:error, e}
end
end
end
@spec publish(Event.t(), event_schema, [publish_opts]) :: return
def publish(event, schema, opts \\ []) do
adapter =
opts[:adapter] || Application.get_env(:pagantis_elixir_tools, ElixirTools.Events)[:adapter]
enveloped_event = add_envelope(event, opts)
with :ok <- validate_json_schema(enveloped_event, schema) do
try do
adapter.publish(enveloped_event)
rescue
e -> {:error, e}
end
end
end
@spec validate(t) :: return
def validate(event) do
with :ok <- validate_name(event.name),
:ok <- validate_payload(event.payload),
:ok <- validate_event_id_seed(event.event_id_seed),
:ok <- validate_event_id_seed_optional(event.event_id_seed_optional),
:ok <- validate_occurred_at(event.occurred_at),
:ok <- validate_version(event.version) do
:ok
end
end
@spec validate_json_schema(map, event_schema) :: return
def validate_json_schema(event, schema) do
stringified_keys_event = to_string_keys(event)
case Validator.validate(schema, stringified_keys_event) do
{:error, [{reason, details} | _]} -> {:error, Enum.join([reason, details], ": ")}
:ok -> :ok
end
end
@spec to_string_keys(map) :: map
defp to_string_keys(event), do: event |> Jason.encode!() |> Jason.decode!()
@spec validate_occurred_at(any) :: return
defp validate_occurred_at(%DateTime{}), do: :ok
defp validate_occurred_at(nil), do: :ok
defp validate_occurred_at(value),
do: {:error, "Expected a DateTime as occurred_at, but got #{inspect(value)}"}
@spec validate_event_id_seed_optional(any) :: return
defp validate_event_id_seed_optional(value) when is_binary(value), do: :ok
defp validate_event_id_seed_optional(value),
do: {:error, "Expected a string as event_id_seed_optional, but got #{inspect(value)}"}
@spec validate_name(any) :: return
defp validate_name(name) do
cond do
!is_binary(name) ->
{:error, "Expected a string as event name, but got #{inspect(name)}"}
!String.contains?(name, "_") ->
{:error, "Expected an underscore in the event name, but got #{name} instead"}
true ->
:ok
end
end
@spec validate_event_id_seed(any) :: return
defp validate_event_id_seed(event_id_seed) do
case UUID.info(event_id_seed) do
{:ok, _} -> :ok
_ -> {:error, "Expected a UUID string as event_id_seed, but got #{inspect(event_id_seed)}"}
end
end
@spec validate_payload(any) :: return
defp validate_payload(payload) when is_map(payload), do: :ok
defp validate_payload(_), do: {:error, "Expected payload to be a map"}
@spec validate_version(any) :: return
defp validate_version(version) do
with {:is_string, true} <- {:is_string, is_binary(version)},
{:split, [major, minor, fix]} <- {:split, String.split(version, ".")},
{:parse_major, {_, ""}} <- {:parse_major, Integer.parse(major)},
{:parse_minor, {_, ""}} <- {:parse_minor, Integer.parse(minor)},
{:parse_fix, {_, ""}} <- {:parse_fix, Integer.parse(fix)} do
:ok
else
{:is_string, false} -> {:error, "Expected a string with a version"}
{:split, _} -> {:error, "Expected version with 3 dots, but received #{version}"}
{:parse_major, _} -> {:error, "Expected a number for the major, but received #{version}"}
{:parse_minor, _} -> {:error, "Expected a number for the minor, but received #{version}"}
{:parse_fix, _} -> {:error, "Expected a number for the fix, but received #{version}"}
end
end
@spec add_envelope(Event.t(), [publish_opts]) :: map
defp add_envelope(event, opts) do
uuid_module = opts[:uuid_module] || UUID
timex_module = opts[:timex_module] || Timex
config = Application.get_env(:pagantis_elixir_tools, ElixirTools.Events)[:adapter_config]
group = Map.fetch!(config, :group)
uuid_seed_2 = "#{event.name}-#{event.version}-#{event.event_id_seed_optional}"
id = uuid_module.uuid5(event.event_id_seed, uuid_seed_2)
occurred_at = event.occurred_at || timex_module.now()
%{
id: id,
action: String.upcase(event.name),
group: group,
occurred_at: Timex.format!(occurred_at, "{ISO:Extended:Z}"),
version: event.version,
payload: event.payload
}
end
end