Skip to main content

lib/rulestead/store/redis.ex

defmodule Rulestead.Store.Redis do
  @moduledoc false
  alias Rulestead.{Redis, Store, StoreError}
  alias Rulestead.Store.Command

  @behaviour Store

  @read_only_message "Redis adapter is read-only"

  @impl Store
  def fetch_snapshot(%Command.FetchSnapshot{} = command) do
    case Redis.client().command(Redis.name(), ["GET", Redis.snapshot_key(command.environment_key)]) do
      {:ok, nil} ->
        {:error, snapshot_not_found(command)}

      {:ok, payload} when is_binary(payload) ->
        decode_snapshot(payload, command)

      {:error, reason} ->
        {:error,
         StoreError.unavailable(
           metadata: %{environment_key: to_string(command.environment_key)},
           cause: reason
         )}
    end
  end

  for callback <- [
        :compare_environments,
        :apply_promotion,
        :preview_manifest_import,
        :apply_manifest_import,
        :fetch_flag,
        :create_flag,
        :update_flag,
        :save_draft_ruleset,
        :publish_ruleset,
        :archive_flag,
        :list_flags,
        :list_environments,
        :list_audiences,
        :list_audience_dependencies,
        :preview_audience_impact,
        :apply_audience_mutation,
        :record_evaluation,
        :advance_rollout,
        :evaluate_guarded_rollout,
        :upsert_rollout_auto_advance_policy,
        :fetch_rollout_auto_advance_policy,
        :evaluate_rollout_auto_advance,
        :fetch_guardrail_status,
        :engage_kill_switch,
        :release_kill_switch,
        :list_audit_events,
        :rollback_audit_event,
        :submit_change_request,
        :approve_change_request,
        :reject_change_request,
        :cancel_change_request,
        :execute_change_request,
        :fetch_change_request,
        :list_change_requests,
        :schedule_change_request,
        :schedule_governed_action,
        :cancel_scheduled_execution,
        :requeue_scheduled_execution,
        :execute_scheduled_execution,
        :fetch_scheduled_execution,
        :list_scheduled_executions,
        :receive_inbound_webhook,
        :fetch_webhook_record,
        :list_webhook_records,
        :create_webhook_destination,
        :update_webhook_destination,
        :fetch_webhook_destination,
        :list_webhook_destinations,
        :list_webhook_deliveries,
        :retry_webhook_delivery
      ] do
    @impl Store
    def unquote(callback)(_command), do: {:error, StoreError.invalid_command(@read_only_message)}
  end

  defp decode_snapshot(payload, command) do
    snapshot = :erlang.binary_to_term(payload, [:safe])

    cond do
      not is_map(snapshot) ->
        {:error, invalid_snapshot(command, snapshot)}

      not is_nil(command.version) and Map.get(snapshot, :version) != command.version ->
        {:error, snapshot_not_found(command)}

      true ->
        {:ok, snapshot}
    end
  rescue
    error in [ArgumentError] ->
      {:error,
       StoreError.unavailable(
         metadata: %{environment_key: to_string(command.environment_key)},
         cause: error
       )}
  end

  defp invalid_snapshot(command, snapshot) do
    StoreError.unavailable(
      metadata: %{environment_key: to_string(command.environment_key)},
      details: [%{message: "redis snapshot payload was invalid"}],
      cause: snapshot
    )
  end

  defp snapshot_not_found(%Command.FetchSnapshot{} = command) do
    metadata =
      %{environment_key: to_string(command.environment_key)}
      |> maybe_put(:version, command.version)

    StoreError.snapshot_not_found(command.environment_key, metadata: metadata)
  end

  defp maybe_put(map, _key, nil), do: map
  defp maybe_put(map, key, value), do: Map.put(map, key, value)
end