asknothingx2-util 0.5.0

asknothing
Documentation
use std::fmt;

use ::http::header::{InvalidHeaderName, InvalidHeaderValue};

pub struct Error {
    inner: Box<Inner>,
}

#[derive(Debug)]
struct Inner {
    kind: Kind,
    message: Option<String>,
    input: Option<String>,
    source: Option<BoxError>,
}

#[derive(Debug)]
pub enum Kind {
    RequestBuild,
    HttpInvalidHeader,
    AuthInvalidScheme,
    ContentTypeInvalid,
    ContentTypeUnsupported,
}

impl Kind {
    pub fn category(self) -> ErrorCategory {
        match self {
            Kind::RequestBuild => ErrorCategory::Request,
            Kind::HttpInvalidHeader => ErrorCategory::Http,
            Kind::AuthInvalidScheme => ErrorCategory::Authentication,
            Kind::ContentTypeInvalid | Kind::ContentTypeUnsupported => ErrorCategory::ContentType,
        }
    }
}

impl fmt::Display for Kind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Kind::RequestBuild => f.write_str("failed to build request"),
            Kind::HttpInvalidHeader => f.write_str("invalid HTTP header"),
            Kind::AuthInvalidScheme => f.write_str("invalid authentication scheme"),
            Kind::ContentTypeInvalid => f.write_str("invalid content type"),
            Kind::ContentTypeUnsupported => f.write_str("unsupported content type"),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
    Request,
    Http,
    Authentication,
    ContentType,
}

type BoxError = Box<dyn std::error::Error + Send + Sync>;

impl Error {
    pub fn new(kind: Kind) -> Self {
        Self {
            inner: Box::new(Inner {
                kind,
                message: None,
                input: None,
                source: None,
            }),
        }
    }

    pub fn with_message(kind: Kind, message: impl Into<String>) -> Self {
        Self {
            inner: Box::new(Inner {
                kind,
                message: Some(message.into()),
                input: None,
                source: None,
            }),
        }
    }

    pub fn with_source(kind: Kind, source: impl Into<BoxError>) -> Self {
        Self {
            inner: Box::new(Inner {
                kind,
                message: None,
                input: None,
                source: Some(source.into()),
            }),
        }
    }

    pub fn with_message_and_source(
        kind: Kind,
        message: impl Into<String>,
        source: impl Into<BoxError>,
    ) -> Self {
        Self {
            inner: Box::new(Inner {
                kind,
                message: Some(message.into()),
                input: None,
                source: Some(source.into()),
            }),
        }
    }

    pub fn with_input(mut self, input: impl Into<String>) -> Self {
        self.inner.input = Some(input.into());
        self
    }

    pub fn message(&self) -> Option<&str> {
        self.inner.message.as_deref()
    }

    pub fn input(&self) -> Option<&str> {
        self.inner.input.as_deref()
    }

    pub fn is_request(&self) -> bool {
        matches!(self.inner.kind, Kind::RequestBuild)
    }
}

impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut builder = f.debug_struct("asknothingx2-util::api::Error");

        builder.field("kind", &self.inner.kind);

        if let Some(ref message) = self.inner.message {
            builder.field("message", message);
        }

        if let Some(ref input) = self.inner.input {
            builder.field("input", input);
        }

        if let Some(ref source) = self.inner.source {
            builder.field("source", source);
        }

        builder.finish()
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(ref message) = self.inner.message {
            write!(f, "{message}")?;
        } else {
            write!(f, "{}", self.inner.kind)?;
        }

        if let Some(ref input) = self.inner.input {
            let truncated = truncate_input(input);
            if !truncated.is_empty() {
                write!(
                    f,
                    " [input: {}{}]",
                    truncated,
                    if input.len() > truncated.len() {
                        "..."
                    } else {
                        ""
                    }
                )?;
            }
        }

        if let Some(ref source) = self.inner.source {
            write!(f, " -> {source})")?;
        }

        Ok(())
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.inner.source.as_ref().map(|e| &**e as _)
    }
}

pub mod request {
    use super::{BoxError, Error, Kind};

    pub fn build<E: Into<BoxError>>(source: E) -> Error {
        Error::with_source(Kind::RequestBuild, source)
    }
}

pub mod http {
    use super::{BoxError, Error, Kind};
    pub fn invalid_header<E: Into<BoxError>>(source: E) -> Error {
        Error::with_source(Kind::HttpInvalidHeader, source)
    }
}

pub mod auth {
    use super::{Error, Kind};

    pub fn invalid_scheme<S: Into<String>>(scheme: S) -> Error {
        Error::with_message(
            Kind::AuthInvalidScheme,
            format!("invalid authentication scheme '{}'", scheme.into()),
        )
    }
}

pub mod content {
    use super::{Error, Kind};

    pub fn invalid_type<T: Into<String>>(content_type: T) -> Error {
        Error::with_message(
            Kind::ContentTypeInvalid,
            format!("invalid content type '{}'", content_type.into()),
        )
    }

    pub fn unsupported<T: Into<String>>(content_type: T) -> Error {
        Error::with_message(
            Kind::ContentTypeUnsupported,
            format!("unsupported content type '{}'", content_type.into()),
        )
    }
}

fn truncate_input(input: &str) -> &str {
    const MAX_LEN: usize = 80;
    if input.len() <= MAX_LEN {
        input
    } else {
        &input[..MAX_LEN]
    }
}

impl From<InvalidHeaderName> for Error {
    fn from(err: InvalidHeaderName) -> Self {
        Error::with_source(Kind::HttpInvalidHeader, err)
    }
}

impl From<InvalidHeaderValue> for Error {
    fn from(err: InvalidHeaderValue) -> Self {
        Error::with_source(Kind::HttpInvalidHeader, err)
    }
}