lib/relyra/request_store.ex

defmodule Relyra.RequestStore do
  @moduledoc """
  Public extension contract for request-intent persistence and one-time consumption.
  """

  alias Relyra.Error

  # Verification anchor: put_intent(relay_state, intent, opts  [])
  @callback put_intent(relay_state :: binary(), intent :: map(), opts :: keyword()) ::
              :ok | {:error, Error.t()}

  # Verification anchor: fetch_intent(relay_state, opts  [])
  @callback fetch_intent(relay_state :: binary(), opts :: keyword()) ::
              {:ok, map()} | {:error, Error.t()}

  # Verification anchor: consume_intent(relay_state, request_id, opts  [])
  @callback consume_intent(relay_state :: binary(), request_id :: binary(), opts :: keyword()) ::
              :ok | {:error, Error.t()}

  @spec put_intent(binary(), map(), keyword()) :: :ok | {:error, Error.t()}
  def put_intent(relay_state, intent, opts \\ [])

  def put_intent(relay_state, intent, opts)
      when is_binary(relay_state) and is_map(intent) and is_list(opts) do
    intent = Map.put_new(intent, :type, :authn)
    dispatch_request_store(request_store(opts), :put_intent, [relay_state, intent, opts])
  end

  @spec fetch_intent(binary(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def fetch_intent(relay_state, opts \\ [])

  def fetch_intent(relay_state, opts) when is_binary(relay_state) and is_list(opts) do
    dispatch_request_store(request_store(opts), :fetch_intent, [relay_state, opts])
  end

  @spec consume_intent(binary(), binary(), keyword()) :: :ok | {:error, Error.t()}
  def consume_intent(relay_state, request_id, opts \\ [])

  def consume_intent(relay_state, request_id, opts)
      when is_binary(relay_state) and is_binary(request_id) and is_list(opts) do
    dispatch_request_store(request_store(opts), :consume_intent, [relay_state, request_id, opts])
  end

  defp request_store(opts) do
    Keyword.get(opts, :request_store, Relyra.RequestStore.Default)
  end

  defp dispatch_request_store(adapter, operation, args)
       when is_atom(adapter) and is_atom(operation) and is_list(args) do
    if Code.ensure_loaded?(adapter) and function_exported?(adapter, operation, length(args)) do
      try do
        case apply(adapter, operation, args) do
          {:ok, result} when is_map(result) -> {:ok, result}
          {:error, %Error{} = error} -> {:error, error}
          :ok -> :ok
          other -> {:error, invalid_adapter_result(adapter, operation, other)}
        end
      rescue
        exception ->
          {:error, adapter_dispatch_error(adapter, operation, Exception.message(exception))}
      catch
        kind, reason ->
          {:error, adapter_dispatch_error(adapter, operation, "#{kind}:#{inspect(reason)}")}
      end
    else
      {:error, adapter_not_configured(adapter, operation)}
    end
  end

  defp dispatch_request_store(adapter, operation, _args) do
    {:error, adapter_not_configured(adapter, operation)}
  end

  defp adapter_not_configured(adapter, operation) do
    Error.new(
      :adapter_not_configured,
      "Request store adapter is unavailable",
      %{
        adapter: inspect(adapter),
        operation: operation,
        hint: "Configure :request_store with a module implementing Relyra.RequestStore"
      }
    )
  end

  defp invalid_adapter_result(adapter, operation, actual) do
    Error.new(
      :adapter_not_configured,
      "Request store adapter returned an invalid tuple",
      %{adapter: inspect(adapter), operation: operation, actual: inspect(actual)}
    )
  end

  defp adapter_dispatch_error(adapter, operation, reason) do
    Error.new(
      :adapter_not_configured,
      "Request store adapter raised during dispatch",
      %{adapter: inspect(adapter), operation: operation, reason: reason}
    )
  end
end