Current section
Files
Jump to
Current section
Files
lib/amp_sdk/error.ex
defmodule AmpSdk.Error do
@moduledoc """
Unified error envelope for SDK operations.
All public tuple-based APIs return `{:error, %AmpSdk.Error{}}` for consistent
pattern matching.
"""
@enforce_keys [:kind, :message]
defexception [:kind, :message, :cause, :details, :context, :exit_code]
@type kind ::
:cli_not_found
| :auth_error
| :command_failed
| :command_timeout
| :command_execution_failed
| :stream_start_failed
| :transport_error
| :transport_exit
| :parse_error
| :invalid_message
| :invalid_configuration
| :config_invalid
| :execution_failed
| :no_result
| :run_deadline_exceeded
| :stream_timeout
| :task_timeout
| :task_exit
| :unsupported_capability
| :unknown
@type t :: %__MODULE__{
kind: kind(),
message: String.t(),
cause: term(),
details: String.t() | nil,
context: map(),
exit_code: integer() | nil
}
@type normalize_opt ::
{:kind, kind()}
| {:message, String.t()}
| {:cause, term()}
| {:details, String.t() | nil}
| {:context, map() | keyword() | nil}
| {:exit_code, integer() | nil}
alias CliSubprocessCore.ProviderCLI.ErrorRuntimeFailure
alias CliSubprocessCore.ProviderFeatures.Error, as: ProviderFeatureError
alias CliSubprocessCore.TransportError, as: CoreTransportError
@spec new(kind(), String.t(), [normalize_opt()]) :: t()
def new(kind, message, opts \\ []) when is_atom(kind) and is_binary(message) do
%__MODULE__{
kind: kind,
message: message,
cause: Keyword.get(opts, :cause),
details: Keyword.get(opts, :details),
context: normalize_context(Keyword.get(opts, :context, %{})),
exit_code: normalize_exit_code(Keyword.get(opts, :exit_code))
}
end
@spec from_runtime_failure(ErrorRuntimeFailure.t(), [normalize_opt()]) :: t()
def from_runtime_failure(%ErrorRuntimeFailure{} = failure, opts \\ []) when is_list(opts) do
extra_context =
opts
|> Keyword.get(:context)
|> normalize_context()
new(
runtime_failure_kind(failure),
Keyword.get(opts, :message, failure.message),
cause: Keyword.get(opts, :cause, failure.cause || failure),
details: Keyword.get(opts, :details, failure.stderr),
context: Map.merge(failure.context || %{}, extra_context),
exit_code: Keyword.get(opts, :exit_code, failure.exit_code)
)
end
@spec normalize(term(), [normalize_opt()]) :: t()
def normalize(reason, opts \\ [])
def normalize(%__MODULE__{} = error, opts) do
error
|> maybe_put(:kind, Keyword.get(opts, :kind))
|> maybe_put(:message, Keyword.get(opts, :message))
|> maybe_put(:cause, Keyword.get(opts, :cause))
|> maybe_put(:details, Keyword.get(opts, :details))
|> maybe_put(:context, normalize_context(Keyword.get(opts, :context, error.context)))
|> maybe_put(:exit_code, normalize_exit_code(Keyword.get(opts, :exit_code)))
end
def normalize(%ErrorRuntimeFailure{} = failure, opts) do
from_runtime_failure(failure, opts)
end
def normalize(%ProviderFeatureError{} = error, opts) do
new(:unsupported_capability, Keyword.get(opts, :message, Exception.message(error)),
cause: error,
details: Keyword.get(opts, :details),
context: %{
provider: error.provider,
feature: error.feature,
option: error.option,
support_state: error.support_state
}
)
end
def normalize(reason, opts) when is_exception(reason) do
case CoreTransportError.reason(reason) do
^reason ->
kind = Keyword.get(opts, :kind, :unknown)
message = Keyword.get(opts, :message, Exception.message(reason))
new(kind, message,
cause: Keyword.get(opts, :cause, reason),
details: Keyword.get(opts, :details),
context: Keyword.get(opts, :context),
exit_code: Keyword.get(opts, :exit_code)
)
_transport_reason ->
normalize_transport_error(reason, opts)
end
end
def normalize(reason, opts) when is_binary(reason) do
kind = Keyword.get(opts, :kind, :unknown)
message = Keyword.get(opts, :message, reason)
new(kind, message,
cause: Keyword.get(opts, :cause, reason),
details: Keyword.get(opts, :details),
context: Keyword.get(opts, :context),
exit_code: Keyword.get(opts, :exit_code)
)
end
def normalize(reason, opts) when is_atom(reason) do
kind = Keyword.get(opts, :kind, :unknown)
new(kind, Keyword.get(opts, :message, Atom.to_string(reason)),
cause: Keyword.get(opts, :cause, reason),
details: Keyword.get(opts, :details),
context: Keyword.get(opts, :context),
exit_code: Keyword.get(opts, :exit_code)
)
end
def normalize({:transport, reason} = tagged_reason, opts) do
if CoreTransportError.match?(reason) do
normalize_transport_error(reason, opts)
else
kind = Keyword.get(opts, :kind, :transport_error)
message = Keyword.get(opts, :message, transport_reason_message(reason))
new(kind, message,
cause: Keyword.get(opts, :cause, tagged_reason),
details: Keyword.get(opts, :details),
context: Keyword.get(opts, :context),
exit_code: Keyword.get(opts, :exit_code)
)
end
end
def normalize(reason, opts), do: normalize_non_transport(reason, opts)
defp normalize_transport_error(error, opts) do
kind = Keyword.get(opts, :kind, :transport_error)
message = Keyword.get(opts, :message, CoreTransportError.message(error))
new(kind, message,
cause: Keyword.get(opts, :cause, error),
details: Keyword.get(opts, :details),
context:
Map.merge(
CoreTransportError.context(error),
normalize_context(Keyword.get(opts, :context))
),
exit_code: Keyword.get(opts, :exit_code)
)
end
defp normalize_non_transport({:task_exit, reason} = tagged_reason, opts) do
kind = Keyword.get(opts, :kind, :task_exit)
message = Keyword.get(opts, :message, "Task exited: #{inspect(reason)}")
new(kind, message,
cause: Keyword.get(opts, :cause, tagged_reason),
details: Keyword.get(opts, :details),
context: Keyword.get(opts, :context),
exit_code: Keyword.get(opts, :exit_code)
)
end
defp normalize_non_transport(reason, opts) do
kind = Keyword.get(opts, :kind, :unknown)
new(kind, Keyword.get(opts, :message, inspect(reason)),
cause: Keyword.get(opts, :cause, reason),
details: Keyword.get(opts, :details),
context: Keyword.get(opts, :context),
exit_code: Keyword.get(opts, :exit_code)
)
end
@spec message(term()) :: String.t()
def message(reason) do
normalize(reason).message
end
defp maybe_put(struct, _field, nil), do: struct
defp maybe_put(struct, field, value), do: Map.put(struct, field, value)
defp normalize_context(nil), do: %{}
defp normalize_context(context) when is_map(context), do: context
defp normalize_context(context) when is_list(context), do: Map.new(context)
defp normalize_context(context), do: %{context: context}
defp normalize_exit_code(code) when is_integer(code), do: code
defp normalize_exit_code(_), do: nil
defp runtime_failure_kind(%ErrorRuntimeFailure{kind: :auth_error}), do: :auth_error
defp runtime_failure_kind(%ErrorRuntimeFailure{kind: :cli_not_found}), do: :cli_not_found
defp runtime_failure_kind(%ErrorRuntimeFailure{kind: :cwd_not_found}), do: :config_invalid
defp runtime_failure_kind(%ErrorRuntimeFailure{kind: :process_exit}), do: :transport_exit
defp runtime_failure_kind(%ErrorRuntimeFailure{kind: :transport_error}), do: :transport_error
defp transport_reason_message(:not_connected), do: "Transport not connected"
defp transport_reason_message(error) do
if CoreTransportError.match?(error) do
CoreTransportError.message(error)
else
"Transport error: #{inspect(error)}"
end
end
end