defmodule OnePiece.Commanded.Helpers do @moduledoc """ A Swiss Army Knife Helper Module. """ alias Commanded.Event.FailureContext @type error_context :: FailureContext.t() | map() @type commanded_dispatch_response :: :ok | {:ok, aggregate_state :: struct()} | {:ok, aggregate_version :: non_neg_integer()} | {:ok, execution_result :: Commanded.Commands.ExecutionResult.t()} | {:error, :unregistered_command} | {:error, :consistency_timeout} | {:error, reason :: term()} @doc """ Deprecated, it has the same behavior as `OnePiece.Commanded.Id.new/0`. """ @spec generate_uuid :: String.t() @deprecated "Use `OnePiece.Commanded.Id.new/0` instead." defdelegate generate_uuid, to: OnePiece.Commanded.Id, as: :new @doc """ Transforms the given `source` map or struct into the `target` struct. """ @spec struct_from(source :: struct(), target :: struct()) :: struct() def struct_from(%_{} = source, target) do struct(target, Map.from_struct(source)) end @spec struct_from(attrs :: map(), target :: module()) :: struct() def struct_from(attrs, target) do struct(target, attrs) end @doc false @spec defines_struct?(mod :: module()) :: boolean() def defines_struct?(mod) do :functions |> mod.__info__() |> Keyword.get(:__struct__) |> Kernel.!=(nil) end @doc """ Copy the information from the `source` map into the given `target` map. iex> OnePiece.Commanded.Helpers.cast_to(%{}, %{name: "ubi-wan", last_name: "kenobi"}, [:last_name]) %{last_name: "kenobi"} """ @spec cast_to(target :: map, source :: map, keys :: [Map.key()]) :: map def cast_to(target, source, keys) do Map.merge(target, Map.take(source, keys)) end @doc """ Returns a keyword list containing the "correlation id" and "causation id" tracing. iex> OnePiece.Commanded.Helpers.tracing_from_metadata(%{ ...> event_id: "26eb06fe-9ba6-4f58-a2dd-2bdba73de4f2", ...> correlation_id: "f634ba94-145c-4fa7-bf7f-0d73dd83b446" ...> }) ...> [causation_id: "26eb06fe-9ba6-4f58-a2dd-2bdba73de4f2", correlation_id: "f634ba94-145c-4fa7-bf7f-0d73dd83b446"] Useful when dispatching commands to copy-forward `t:Commanded.Event.Handler.metadata/0` tracing information. defmodule MyProcessor do application: MyApp, use Commanded.Event.Handler, name: "my_processor" alias OnePiece.Commanded.Helpers def handle(%MyEvent{} = event, metadata) do MyApp.dispatch( %MyCommand{}, # copy-forward the information Helpers.tracing_from_metadata(metadata) ) end end """ @spec tracing_from_metadata(metadata :: Commanded.Event.Handler.metadata()) :: [ causation_id: String.t(), correlation_id: String.t() ] def tracing_from_metadata(metadata) do [causation_id: metadata.event_id, correlation_id: metadata.correlation_id] end @doc """ Adds the "correlation id" and "causation id" tracing to an existing keyword list configuration option. iex> OnePiece.Commanded.Helpers.tracing_from_metadata([timeout: 30_000], %{ ...> event_id: "26eb06fe-9ba6-4f58-a2dd-2bdba73de4f2", ...> correlation_id: "f634ba94-145c-4fa7-bf7f-0d73dd83b446" ...> }) ...> [timeout: 30_000, causation_id: "26eb06fe-9ba6-4f58-a2dd-2bdba73de4f2", correlation_id: "f634ba94-145c-4fa7-bf7f-0d73dd83b446"] Useful when dispatching commands to copy-forward the `t:Commanded.Event.Handler.metadata/0` tracing information and wants to also add other keyword list options. defmodule MyProcessor do use Commanded.Event.Handler, application: MyApp, name: "my_processor" alias OnePiece.Commanded.Helpers def handle(%MyEvent{} = event, metadata) do MyApp.dispatch( %MyCommand{}, # copy-forward the information Helpers.tracing_from_metadata([timeout: 30_000], metadata) ) end end """ @spec tracing_from_metadata(opts :: keyword, metadata :: Commanded.Event.Handler.metadata()) :: [ causation_id: String.t(), correlation_id: String.t() ] def tracing_from_metadata(opts, metadata) do Keyword.merge(opts, tracing_from_metadata(metadata)) end @doc """ Returns `skip` or a `retry` response. When the `c:Commanded.Application.dispatch/1` or `c:Commanded.Application.dispatch/2` returns an `:skip` otherwise, returns a `:retry` response. Useful when you are doing error handling in your `c:Commanded.Event.Handler.error/3`. iex> success_dispatch = fn _ -> :ok end ...> OnePiece.Commanded.Helpers.skip_or_retry(success_dispatch.(%{}), %{}) :skip iex> success_dispatch = fn _ -> {:ok, %{}} end ...> OnePiece.Commanded.Helpers.skip_or_retry(success_dispatch.(%{}), %{}) :skip iex> failure_dispatch = fn _ -> {:error, :ooops} end ...> OnePiece.Commanded.Helpers.skip_or_retry(failure_dispatch.(%{}), %{failures: 1}) {:retry, %{failures: 1}} """ @spec skip_or_retry(tuple_response :: commanded_dispatch_response, context :: error_context) :: :skip | {:retry, error_context} def skip_or_retry(:ok, _context), do: :skip def skip_or_retry({:ok, _}, _context), do: :skip def skip_or_retry(_, context), do: {:retry, context} @doc """ Returns `skip` or a `retry` response with a given delay. When the `c:Commanded.Application.dispatch/1` or `c:Commanded.Application.dispatch/2` returns an `:skip` otherwise, returns a `:retry` response. Useful when you are doing error handling in your `c:Commanded.Event.Handler.error/3`. iex> success_dispatch = fn _ -> :ok end ...> OnePiece.Commanded.Helpers.skip_or_retry(success_dispatch.(%{}), 5_000, %{}) :skip iex> success_dispatch = fn _ -> {:ok, %{}} end ...> OnePiece.Commanded.Helpers.skip_or_retry(success_dispatch.(%{}), 5_000, %{}) :skip iex> failure_dispatch = fn _ -> {:error, :ooops} end ...> OnePiece.Commanded.Helpers.skip_or_retry(failure_dispatch.(%{}), 5_000, %{failures: 1}) {:retry, 5_000, %{failures: 1}} """ @spec skip_or_retry( tuple_response :: commanded_dispatch_response, delay :: non_neg_integer(), context :: error_context ) :: :skip | {:retry, non_neg_integer(), error_context} def skip_or_retry(:ok, _delay, _context), do: :skip def skip_or_retry({:ok, _}, _delay, _context), do: :skip def skip_or_retry(_, delay, context), do: {:retry, delay, context} @doc """ Increase the failure counter from `t:Commanded.Event.FailureContext.t/0` context by one. iex> OnePiece.Commanded.Helpers.increase_failure_counter(%Commanded.Event.FailureContext{context: %{failures_count: 1}}) %{failures_count: 2} """ @spec increase_failure_counter(failure_context :: FailureContext.t()) :: map() def increase_failure_counter(%FailureContext{} = failure_context) do Map.update(failure_context.context, :failures_count, 1, &(&1 + 1)) end @doc """ Ignores a specific error from a command dispatch result. This function takes a dispatch result and an error term. If the result represents an error that matches the provided error term, the function returns `:ok`. Otherwise, it returns the original result. This is useful when an error condition should be treated as a successful operation under specific circumstances. ## Examples iex> OnePiece.Commanded.Helpers.ignore_error({:error, :idempotency_failure}, :idempotency_failure) :ok iex> OnePiece.Commanded.Helpers.ignore_error({:error, :something_went_wrong}, :idempotency_failure) {:error, :something_went_wrong} iex> OnePiece.Commanded.Helpers.ignore_error(:ok, :idempotency_failure) :ok iex> OnePiece.Commanded.Helpers.ignore_error({:ok, %{name: "Billy"}}, :idempotency_failure) {:ok, %{name: "Billy"}} """ @spec ignore_error(result :: commanded_dispatch_response, error: any) :: commanded_dispatch_response def ignore_error({:error, expected_error}, expected_error), do: :ok def ignore_error(result, _expected_error), do: result @doc false def get_primary_key({identifier, identifier_type}) do {identifier, identifier_type} end def get_primary_key(identifier) when is_atom(identifier) do {identifier, :string} end end