use std::fmt;
use std::sync::Arc;
use base64::Engine as _;
use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
use buffa::Message as _;
use crate::proto::pethost::panel::v1 as wire;
use crate::types;
#[derive(Debug, Clone)]
pub struct Error {
code: ErrorCode,
message: String,
details: Vec<ErrorDetail>,
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
}
impl Error {
pub fn code(&self) -> ErrorCode {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
pub fn details(&self) -> &[ErrorDetail] {
&self.details
}
pub(crate) fn new(code: ErrorCode, message: String) -> Self {
Self {
code,
message,
details: Vec::new(),
source: None,
}
}
pub(crate) fn from_wire(error: connectrpc::ConnectError) -> Self {
let source = error.source_arc();
let details = error.details.iter().filter_map(detail_of).collect();
Self {
code: ErrorCode::from_wire(error.code),
message: error.message.unwrap_or_default(),
details,
source,
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source.as_deref().map(|source| source as _)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
Canceled,
Unknown,
InvalidArgument,
DeadlineExceeded,
NotFound,
AlreadyExists,
PermissionDenied,
ResourceExhausted,
FailedPrecondition,
Aborted,
OutOfRange,
Unimplemented,
Internal,
Unavailable,
DataLoss,
Unauthenticated,
}
impl ErrorCode {
pub fn as_str(self) -> &'static str {
match self {
Self::Canceled => "canceled",
Self::Unknown => "unknown",
Self::InvalidArgument => "invalid_argument",
Self::DeadlineExceeded => "deadline_exceeded",
Self::NotFound => "not_found",
Self::AlreadyExists => "already_exists",
Self::PermissionDenied => "permission_denied",
Self::ResourceExhausted => "resource_exhausted",
Self::FailedPrecondition => "failed_precondition",
Self::Aborted => "aborted",
Self::OutOfRange => "out_of_range",
Self::Unimplemented => "unimplemented",
Self::Internal => "internal",
Self::Unavailable => "unavailable",
Self::DataLoss => "data_loss",
Self::Unauthenticated => "unauthenticated",
}
}
fn from_wire(code: connectrpc::ErrorCode) -> Self {
use connectrpc::ErrorCode as Wire;
match code {
Wire::Canceled => Self::Canceled,
Wire::Unknown => Self::Unknown,
Wire::InvalidArgument => Self::InvalidArgument,
Wire::DeadlineExceeded => Self::DeadlineExceeded,
Wire::NotFound => Self::NotFound,
Wire::AlreadyExists => Self::AlreadyExists,
Wire::PermissionDenied => Self::PermissionDenied,
Wire::ResourceExhausted => Self::ResourceExhausted,
Wire::FailedPrecondition => Self::FailedPrecondition,
Wire::Aborted => Self::Aborted,
Wire::OutOfRange => Self::OutOfRange,
Wire::Unimplemented => Self::Unimplemented,
Wire::Internal => Self::Internal,
Wire::Unavailable => Self::Unavailable,
Wire::DataLoss => Self::DataLoss,
Wire::Unauthenticated => Self::Unauthenticated,
_ => Self::Unknown,
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum ErrorDetail {
ProjectBusy(types::ProjectBusy),
ProjectChanged(types::ProjectChanged),
MachineUnreachable(types::MachineUnreachable),
NoMachine(types::NoMachine),
}
fn detail_of(detail: &connectrpc::ErrorDetail) -> Option<ErrorDetail> {
let bytes = detail_bytes(detail)?;
let name = detail.type_url.rsplit('/').next().unwrap_or_default();
match name {
"pethost.panel.v1.ProjectBusy" => wire::ProjectBusy::decode_from_slice(&bytes)
.ok()
.map(|message| ErrorDetail::ProjectBusy(message.into())),
"pethost.panel.v1.ProjectChanged" => wire::ProjectChanged::decode_from_slice(&bytes)
.ok()
.map(|message| ErrorDetail::ProjectChanged(message.into())),
"pethost.panel.v1.MachineUnreachable" => wire::MachineUnreachable::decode_from_slice(&bytes)
.ok()
.map(|message| ErrorDetail::MachineUnreachable(message.into())),
"pethost.panel.v1.NoMachine" => wire::NoMachine::decode_from_slice(&bytes)
.ok()
.map(|message| ErrorDetail::NoMachine(message.into())),
_ => None,
}
}
fn detail_bytes(detail: &connectrpc::ErrorDetail) -> Option<Vec<u8>> {
let value = detail.value.as_deref()?;
STANDARD_NO_PAD.decode(value).or_else(|_| STANDARD.decode(value)).ok()
}