Skip to main content

pethost/
error.rs

1// @generated by protoc-gen-panel-rust from v1/panel.proto. DO NOT EDIT.
2
3use 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/// Why a call failed: a code to branch on, a message for people, and what the API attached.
14///
15/// ```
16/// use pethost::{Error, ErrorCode};
17///
18/// fn report(error: &Error) {
19///     match (error.code(), error.details()) {
20///         (ErrorCode::Unauthenticated, _) => eprintln!("the token is wrong or revoked"),
21///         (ErrorCode::Unavailable, [detail, ..]) => eprintln!("retry later: {detail:?}"),
22///         _ => eprintln!("{error}"),
23///     }
24/// }
25/// ```
26///
27/// A call that never reached the API (no network, a refused connection, a bad certificate) is
28/// [`ErrorCode::Unavailable`] too, with the cause as its
29/// [`source`](std::error::Error::source).
30#[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    /// What kind of failure it is: the one thing to branch on.
40    pub fn code(&self) -> ErrorCode {
41        self.code
42    }
43
44    /// The API's sentence for people: what is wrong and what to do. Never parse it.
45    pub fn message(&self) -> &str {
46        &self.message
47    }
48
49    /// What the API attached, of the kinds this version of the crate knows. A kind added to
50    /// the API later is left out.
51    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/// The error codes of the Connect protocol. The crate's front page says what each means in
90/// this API.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum ErrorCode {
93    /// The call was cancelled.
94    Canceled,
95    /// An error of no other code.
96    Unknown,
97    /// A bad or unknown field: the message names it.
98    InvalidArgument,
99    /// The deadline passed before the call ended.
100    DeadlineExceeded,
101    /// No such entity.
102    NotFound,
103    /// The entity exists already.
104    AlreadyExists,
105    /// The caller may not do this.
106    PermissionDenied,
107    /// A limit was reached.
108    ResourceExhausted,
109    /// The system is not in the state the call needs.
110    FailedPrecondition,
111    /// Changed since read: read again, then redo.
112    Aborted,
113    /// Outside the range that is kept.
114    OutOfRange,
115    /// The API does not have this call.
116    Unimplemented,
117    /// The API failed on its own side.
118    Internal,
119    /// Retry later.
120    Unavailable,
121    /// Data was lost.
122    DataLoss,
123    /// The token is missing, wrong or revoked.
124    Unauthenticated,
125}
126
127impl ErrorCode {
128    /// The code as the protocol writes it: `"not_found"`.
129    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            // The wire layer's enum is open; the protocol has no seventeenth code.
171            _ => 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/// What the API attaches to an [`Error`] to branch on, beyond its code.
183#[derive(Debug, Clone, PartialEq)]
184#[non_exhaustive]
185#[allow(clippy::large_enum_variant)]
186pub enum ErrorDetail {
187    /// Detail of an [`ErrorCode::Unavailable`](crate::ErrorCode::Unavailable) error: an operation, or a short action, holds the project. The message says the same in words.
188    ProjectBusy(types::ProjectBusy),
189    /// Detail of an [`ErrorCode::Aborted`](crate::ErrorCode::Aborted) error: `base_deploy_id` is not the current deploy: the project was deployed since you read it. Read it again ([`Project::deploy_id`](crate::types::Project::deploy_id)) and redo your change on its files.
190    ProjectChanged(types::ProjectChanged),
191    /// Detail of an [`ErrorCode::Unavailable`](crate::ErrorCode::Unavailable) error: the machine does not answer now, as while it restarts. Call again later.
192    MachineUnreachable(types::MachineUnreachable),
193    /// Detail of a [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) error: the account has no machine to run projects on yet.
194    NoMachine(types::NoMachine),
195}
196
197/// A detail of a kind this version of the crate knows; `None` = of a later kind, or unreadable.
198fn 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
219/// The bytes of a detail: the protocol writes them in base64 without padding, and takes it
220/// with.
221fn 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}