pethost 0.2.9

Client for the API of Pethost, managed hosting for Docker Compose projects: deploy, logs, HTTP traffic, files
Documentation
// @generated by protoc-gen-panel-rust from v1/panel.proto. DO NOT EDIT.

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;

/// Why a call failed: a code to branch on, a message for people, and what the API attached.
///
/// ```
/// use pethost::{Error, ErrorCode};
///
/// fn report(error: &Error) {
///     match (error.code(), error.details()) {
///         (ErrorCode::Unauthenticated, _) => eprintln!("the token is wrong or revoked"),
///         (ErrorCode::Unavailable, [detail, ..]) => eprintln!("retry later: {detail:?}"),
///         _ => eprintln!("{error}"),
///     }
/// }
/// ```
///
/// A call that never reached the API (no network, a refused connection, a bad certificate) is
/// [`ErrorCode::Unavailable`] too, with the cause as its
/// [`source`](std::error::Error::source).
#[derive(Debug, Clone)]
pub struct Error {
    code: ErrorCode,
    message: String,
    details: Vec<ErrorDetail>,
    source: Option<Arc<dyn std::error::Error + Send + Sync>>,
}

impl Error {
    /// What kind of failure it is: the one thing to branch on.
    pub fn code(&self) -> ErrorCode {
        self.code
    }

    /// The API's sentence for people: what is wrong and what to do. Never parse it.
    pub fn message(&self) -> &str {
        &self.message
    }

    /// What the API attached, of the kinds this version of the crate knows. A kind added to
    /// the API later is left out.
    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 _)
    }
}

/// The error codes of the Connect protocol. The crate's front page says what each means in
/// this API.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorCode {
    /// The call was cancelled.
    Canceled,
    /// An error of no other code.
    Unknown,
    /// A bad or unknown field: the message names it.
    InvalidArgument,
    /// The deadline passed before the call ended.
    DeadlineExceeded,
    /// No such entity.
    NotFound,
    /// The entity exists already.
    AlreadyExists,
    /// The caller may not do this.
    PermissionDenied,
    /// A limit was reached.
    ResourceExhausted,
    /// The system is not in the state the call needs.
    FailedPrecondition,
    /// Changed since read: read again, then redo.
    Aborted,
    /// Outside the range that is kept.
    OutOfRange,
    /// The API does not have this call.
    Unimplemented,
    /// The API failed on its own side.
    Internal,
    /// Retry later.
    Unavailable,
    /// Data was lost.
    DataLoss,
    /// The token is missing, wrong or revoked.
    Unauthenticated,
}

impl ErrorCode {
    /// The code as the protocol writes it: `"not_found"`.
    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,
            // The wire layer's enum is open; the protocol has no seventeenth code.
            _ => Self::Unknown,
        }
    }
}

impl fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

/// What the API attaches to an [`Error`] to branch on, beyond its code.
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum ErrorDetail {
    /// 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.
    ProjectBusy(types::ProjectBusy),
    /// 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.
    ProjectChanged(types::ProjectChanged),
    /// Detail of an [`ErrorCode::Unavailable`](crate::ErrorCode::Unavailable) error: the machine does not answer now, as while it restarts. Call again later.
    MachineUnreachable(types::MachineUnreachable),
    /// Detail of a [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) error: the machine's software predates what was asked. It comes with the machine's next update, which Pethost runs; nothing for the caller to do.
    MachineUpdatePending(types::MachineUpdatePending),
    /// Detail of a [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) error from [`Client::link_database`](crate::Client::link_database) or `CreateDatabase`: a project of `link` cannot be linked to the database. Nothing changed.
    DatabaseLinkRefusal(types::DatabaseLinkRefusal),
    /// Detail of a [`ErrorCode::FailedPrecondition`](crate::ErrorCode::FailedPrecondition) error: the account has no machine to run projects on yet.
    NoMachine(types::NoMachine),
}

/// A detail of a kind this version of the crate knows; `None` = of a later kind, or unreadable.
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.MachineUpdatePending" => wire::MachineUpdatePending::decode_from_slice(&bytes)
            .ok()
            .map(|message| ErrorDetail::MachineUpdatePending(message.into())),
        "pethost.panel.v1.DatabaseLinkRefusal" => wire::DatabaseLinkRefusal::decode_from_slice(&bytes)
            .ok()
            .map(|message| ErrorDetail::DatabaseLinkRefusal(message.into())),
        "pethost.panel.v1.NoMachine" => wire::NoMachine::decode_from_slice(&bytes)
            .ok()
            .map(|message| ErrorDetail::NoMachine(message.into())),
        _ => None,
    }
}

/// The bytes of a detail: the protocol writes them in base64 without padding, and takes it
/// with.
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()
}