Skip to main content

lib/jidoka/review/response.ex

defmodule Jidoka.Review.Response do
  @moduledoc """
  Application response to a pending review request.

  A response is intentionally small: it targets a single interrupt and either
  approves or denies the pending operation.
  """

  alias Jidoka.Review.Interrupt
  alias Jidoka.Schema

  @decisions [:approved, :denied]

  @schema Zoi.struct(
            __MODULE__,
            %{
              interrupt_id: Schema.non_empty_string(),
              decision: Schema.atom_enum(@decisions),
              reason: Zoi.any() |> Zoi.nullish(),
              responded_at_ms: Zoi.integer() |> Zoi.gte(0) |> Zoi.nullish(),
              metadata: Zoi.map() |> Zoi.default(%{})
            },
            coerce: true
          )

  @type decision :: :approved | :denied
  @type t :: unquote(Zoi.type_spec(@schema))
  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  @spec schema() :: Zoi.schema()
  def schema, do: @schema

  @spec decisions() :: [decision()]
  def decisions, do: @decisions

  @spec new(keyword() | map()) :: {:ok, t()} | {:error, term()}
  def new(attrs), do: Schema.parse(@schema, attrs)

  @spec new!(keyword() | map()) :: t()
  def new!(attrs), do: Schema.parse!(@schema, attrs, "review response")

  @spec from_input(t() | keyword() | map()) :: {:ok, t()} | {:error, term()}
  def from_input(%__MODULE__{} = response), do: new(response)
  def from_input(input), do: new(input)

  @spec approve(Interrupt.t() | String.t(), keyword()) :: t()
  def approve(interrupt_or_id, opts \\ []) do
    new!(
      interrupt_id: interrupt_id(interrupt_or_id),
      decision: :approved,
      reason: Keyword.get(opts, :reason),
      responded_at_ms: Keyword.get(opts, :responded_at_ms),
      metadata: Keyword.get(opts, :metadata, %{})
    )
  end

  @spec deny(Interrupt.t() | String.t(), keyword()) :: t()
  def deny(interrupt_or_id, opts \\ []) do
    new!(
      interrupt_id: interrupt_id(interrupt_or_id),
      decision: :denied,
      reason: Keyword.get(opts, :reason),
      responded_at_ms: Keyword.get(opts, :responded_at_ms),
      metadata: Keyword.get(opts, :metadata, %{})
    )
  end

  defp interrupt_id(%Interrupt{id: id}), do: id
  defp interrupt_id(id) when is_binary(id), do: id
end