hook0-client 2.0.3

Rust SDK for Hook0 Open-Source Webhooks as a service for SaaS
Documentation
// Generated by hook0-sdkgen from the OpenAPI snapshot the API crate commits.
// Do not edit by hand: run `UPDATE_SDK=rust cargo test -p hook0-sdkgen sdk_targets` and commit the result.
//! The failures the API reports: the document it describes one with, and the one error
//! every generated method answers instead of what it was asked for.

use super::models::Problem;
use super::models::ProblemId;

/// Lowest status the API answers a success under.
const LOWEST_SUCCESS: u16 = 200;

/// Lowest status that is no longer a success.
const LOWEST_REDIRECTION: u16 = 300;

/// Longest fragment of an answer a message carries.
///
/// Bodies are written by a server this crate does not control, so they are cut at a fixed
/// budget rather than echoed whole into whatever the caller logs.
const MAX_PREVIEW_BYTES: usize = 256;

/// What the API answered when it did not answer a success.
///
/// Every problem the document names is one of these, told apart by the kind it carries;
/// the document the API sent, when it sent one this crate can read, is beside it. A body
/// naming no problem still reaches a caller as one of these, carrying no kind.
#[derive(Debug, Clone)]
pub struct ProblemError {
    /// Status the API answered under.
    pub status: u16,
    /// Problem the API named, absent when the body named none.
    pub kind: Option<ProblemId>,
    /// Document the API answered, absent when it answered none this crate can read.
    pub problem: Option<Problem>,
    /// What to say about the failure, as much of the API's answer as fits included.
    pub detail: String,
}

impl std::fmt::Display for ProblemError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        formatter.write_str(&self.detail)
    }
}

impl std::error::Error for ProblemError {}

/// Everything a generated method answers instead of what it was asked for.
#[derive(Debug)]
pub enum RequestError {
    /// The request never reached the API, or its answer never came back.
    Transport(Box<dyn std::error::Error + Send + Sync>),
    /// The API answered a failure, and described it.
    ///
    /// Held behind a pointer, so that every method answers a small error whatever the
    /// document declares a failure body to carry.
    Api(Box<ProblemError>),
    /// The API answered something this crate cannot read.
    Unreadable {
        /// Status the API answered under.
        status: u16,
        /// What was answered, as much of it as fits included.
        detail: String,
    },
    /// The body of the request could not be written.
    Unwritable {
        /// Why it could not be written.
        detail: String,
    },
}

impl RequestError {
    /// Reports that the request never reached the API.
    pub fn transport<E>(cause: E) -> Self
    where
        E: std::error::Error + Send + Sync + 'static,
    {
        Self::Transport(Box::new(cause))
    }

    /// Reports a request body this crate could not write.
    pub fn unwritable(cause: serde_json::Error) -> Self {
        Self::Unwritable {
            detail: cause.to_string(),
        }
    }

    /// Reports an answer this crate could not read.
    pub fn unreadable(status: u16, payload: &[u8], cause: &serde_json::Error) -> Self {
        let seen = preview(payload);
        let unread = format!("the API answered {status} with an unreadable body");
        let detail = format!("{unread}: {cause} ({seen})");
        Self::Unreadable { status, detail }
    }
}

impl std::fmt::Display for RequestError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Transport(cause) => write!(formatter, "the API was not reached: {cause}"),
            Self::Api(failure) => failure.fmt(formatter),
            Self::Unreadable { detail, .. } => formatter.write_str(detail),
            Self::Unwritable { detail } => formatter.write_str(detail),
        }
    }
}

impl std::error::Error for RequestError {}

/// The failure the API reported, and nothing at all when what it answered was a success.
pub fn problem_for(status: u16, payload: &[u8]) -> Option<ProblemError> {
    if (LOWEST_SUCCESS..LOWEST_REDIRECTION).contains(&status) {
        return None;
    }

    let problem: Option<Problem> = serde_json::from_slice(payload).ok();
    let seen = preview(payload);
    let kind = problem.as_ref().map(|problem| problem.id);

    Some(ProblemError {
        status,
        kind,
        problem,
        detail: format!("the API answered {status}: {seen}"),
    })
}

/// As much of an answer as a message may carry, with whatever was not text left out.
fn preview(payload: &[u8]) -> String {
    let text = String::from_utf8_lossy(payload);
    let mut kept = String::with_capacity(MAX_PREVIEW_BYTES);

    for character in text.chars() {
        if kept.len() + character.len_utf8() > MAX_PREVIEW_BYTES {
            kept.push('');
            break;
        }
        kept.push(character);
    }

    kept
}