1use std::fmt;
4use std::sync::Arc;
5
6use base64::Engine as _;
7use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
8use buffa::Message as _;
9
10use crate::proto::pethost::panel::v1 as wire;
11use crate::types;
12
13#[derive(Debug, Clone)]
31pub struct Error {
32 code: ErrorCode,
33 message: String,
34 details: Vec<ErrorDetail>,
35 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
36}
37
38impl Error {
39 pub fn code(&self) -> ErrorCode {
41 self.code
42 }
43
44 pub fn message(&self) -> &str {
46 &self.message
47 }
48
49 pub fn details(&self) -> &[ErrorDetail] {
52 &self.details
53 }
54
55 pub(crate) fn new(code: ErrorCode, message: String) -> Self {
56 Self {
57 code,
58 message,
59 details: Vec::new(),
60 source: None,
61 }
62 }
63
64 pub(crate) fn from_wire(error: connectrpc::ConnectError) -> Self {
65 let source = error.source_arc();
66 let details = error.details.iter().filter_map(detail_of).collect();
67
68 Self {
69 code: ErrorCode::from_wire(error.code),
70 message: error.message.unwrap_or_default(),
71 details,
72 source,
73 }
74 }
75}
76
77impl fmt::Display for Error {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 write!(f, "{}: {}", self.code, self.message)
80 }
81}
82
83impl std::error::Error for Error {
84 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
85 self.source.as_deref().map(|source| source as _)
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum ErrorCode {
93 Canceled,
95 Unknown,
97 InvalidArgument,
99 DeadlineExceeded,
101 NotFound,
103 AlreadyExists,
105 PermissionDenied,
107 ResourceExhausted,
109 FailedPrecondition,
111 Aborted,
113 OutOfRange,
115 Unimplemented,
117 Internal,
119 Unavailable,
121 DataLoss,
123 Unauthenticated,
125}
126
127impl ErrorCode {
128 pub fn as_str(self) -> &'static str {
130 match self {
131 Self::Canceled => "canceled",
132 Self::Unknown => "unknown",
133 Self::InvalidArgument => "invalid_argument",
134 Self::DeadlineExceeded => "deadline_exceeded",
135 Self::NotFound => "not_found",
136 Self::AlreadyExists => "already_exists",
137 Self::PermissionDenied => "permission_denied",
138 Self::ResourceExhausted => "resource_exhausted",
139 Self::FailedPrecondition => "failed_precondition",
140 Self::Aborted => "aborted",
141 Self::OutOfRange => "out_of_range",
142 Self::Unimplemented => "unimplemented",
143 Self::Internal => "internal",
144 Self::Unavailable => "unavailable",
145 Self::DataLoss => "data_loss",
146 Self::Unauthenticated => "unauthenticated",
147 }
148 }
149
150 fn from_wire(code: connectrpc::ErrorCode) -> Self {
151 use connectrpc::ErrorCode as Wire;
152
153 match code {
154 Wire::Canceled => Self::Canceled,
155 Wire::Unknown => Self::Unknown,
156 Wire::InvalidArgument => Self::InvalidArgument,
157 Wire::DeadlineExceeded => Self::DeadlineExceeded,
158 Wire::NotFound => Self::NotFound,
159 Wire::AlreadyExists => Self::AlreadyExists,
160 Wire::PermissionDenied => Self::PermissionDenied,
161 Wire::ResourceExhausted => Self::ResourceExhausted,
162 Wire::FailedPrecondition => Self::FailedPrecondition,
163 Wire::Aborted => Self::Aborted,
164 Wire::OutOfRange => Self::OutOfRange,
165 Wire::Unimplemented => Self::Unimplemented,
166 Wire::Internal => Self::Internal,
167 Wire::Unavailable => Self::Unavailable,
168 Wire::DataLoss => Self::DataLoss,
169 Wire::Unauthenticated => Self::Unauthenticated,
170 _ => Self::Unknown,
172 }
173 }
174}
175
176impl fmt::Display for ErrorCode {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 f.write_str(self.as_str())
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
184#[non_exhaustive]
185#[allow(clippy::large_enum_variant)]
186pub enum ErrorDetail {
187 ProjectBusy(types::ProjectBusy),
189 ProjectChanged(types::ProjectChanged),
191 MachineUnreachable(types::MachineUnreachable),
193 NoMachine(types::NoMachine),
195}
196
197fn detail_of(detail: &connectrpc::ErrorDetail) -> Option<ErrorDetail> {
199 let bytes = detail_bytes(detail)?;
200 let name = detail.type_url.rsplit('/').next().unwrap_or_default();
201
202 match name {
203 "pethost.panel.v1.ProjectBusy" => wire::ProjectBusy::decode_from_slice(&bytes)
204 .ok()
205 .map(|message| ErrorDetail::ProjectBusy(message.into())),
206 "pethost.panel.v1.ProjectChanged" => wire::ProjectChanged::decode_from_slice(&bytes)
207 .ok()
208 .map(|message| ErrorDetail::ProjectChanged(message.into())),
209 "pethost.panel.v1.MachineUnreachable" => wire::MachineUnreachable::decode_from_slice(&bytes)
210 .ok()
211 .map(|message| ErrorDetail::MachineUnreachable(message.into())),
212 "pethost.panel.v1.NoMachine" => wire::NoMachine::decode_from_slice(&bytes)
213 .ok()
214 .map(|message| ErrorDetail::NoMachine(message.into())),
215 _ => None,
216 }
217}
218
219fn detail_bytes(detail: &connectrpc::ErrorDetail) -> Option<Vec<u8>> {
222 let value = detail.value.as_deref()?;
223 STANDARD_NO_PAD.decode(value).or_else(|_| STANDARD.decode(value)).ok()
224}