git-bot-feedback 0.5.3

A library designed for CI tools that posts comments on a Pull Request.
Documentation
//! Error types used across the git-bot-feedback crate.
#[cfg(feature = "file-changes")]
use std::path::PathBuf;

use chrono::{DateTime, Utc};
use thiserror::Error;

use crate::client::MAX_RETRIES;

/// The possible errors emitted when parsing git diffs.
#[derive(Debug, thiserror::Error)]
#[cfg(feature = "file-changes")]
#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
pub enum DiffError {
    /// An error emitted when failing to compile a Regular expression pattern.
    #[error("Failed to compile regex pattern: {0}")]
    RegExCompileFailed(#[from] regex::Error),
}

/// The possible errors emitted when validating an [`OutputVariable`](struct@crate::OutputVariable).
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum OutputVariableError {
    /// The output variable's name is empty.
    #[error("The output variable's name is empty")]
    NameIsEmpty,
    /// The output variable's name starts with a number.
    #[error("The output variable's name starts with a number: '{0}'")]
    NameStartsWithNumber(String),
    /// The output variable's name contains non-printable characters.
    #[error("The output variable's name contains non-printable characters: '{0}'")]
    NameContainsNonPrintableCharacters(String),
    /// The output variable's value contains non-printable characters.
    #[error("The output variable's value contains non-printable characters: '{0}'")]
    ValueContainsNonPrintableCharacters(String),
    /// Unsupported CI platform.
    #[error("Unsupported CI platform")]
    UnsupportedPlatform,
}

/// The possible error emitted by the REST client API
#[derive(Debug, Error)]
pub enum RestClientError {
    /// Errors related to parsing git diffs.
    #[error(transparent)]
    #[cfg(feature = "file-changes")]
    #[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
    DiffError(#[from] DiffError),

    /// Error emitted when encountering malformed event information.
    #[error("Encountered malformed event info: {0}")]
    MalformedEventInfo(String),

    /// Error related to making HTTP requests
    #[error(transparent)]
    Request(#[from] reqwest::Error),

    /// Error related to making HTTP requests, with additional context about the request that caused the error.
    #[error("Failed to {task}: {source}")]
    RequestContext {
        /// The task being attempted.
        task: String,
        /// The original error being propagated.
        #[source]
        source: reqwest::Error,
    },

    /// Errors related to standard I/O.
    #[error("Failed to {task}: {source}")]
    Io {
        /// The task being attempted.
        task: String,
        /// The original error being propagated.
        #[source]
        source: std::io::Error,
    },

    /// Error related to `git` command execution.
    #[error("Git command error: {0}")]
    #[cfg(feature = "file-changes")]
    #[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
    GitCommand(String),

    /// Error related to exceeding REST API Rate limits and
    /// no reset time is provided in the response headers.
    #[error("Primary Rate Limit exceeded (no reset time provided)")]
    RateLimitNoReset,

    /// Error related to exceeding REST API Rate limits with a known reset time.
    #[error("Primary Rate Limit exceeded; resets at {0}")]
    RateLimitPrimary(DateTime<Utc>),

    /// Error related to exhausting all retries after hitting REST API Rate limits.
    #[error("Rate Limit exceeded after all {MAX_RETRIES} retries exhausted")]
    RateLimitSecondary,

    /// Error emitted when failing to clone a request object.
    #[error("Failed to clone request object for auto-retries")]
    CannotCloneRequest,

    /// Error emitted when failing to create a header value.
    #[error("Tried to create a header value from invalid string data")]
    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),

    /// Error emitted when failing to convert a header value to string.
    #[error("Failed to convert header value to string")]
    UnexpectedHeaderValue(#[from] reqwest::header::ToStrError),

    /// Error emitted when failing to parse an integer from a header value (as a UTF-8 string).
    #[error("Failed to parse integer from header value: {0}")]
    HeaderParseInt(#[from] std::num::ParseIntError),

    /// Error emitted when failing to parse a URL.
    #[error("Failed to parse URL:{0}")]
    UrlParse(#[from] url::ParseError),

    /// Error emitted when failing to deserialize/serialize request/response JSON data.
    #[error("Failed to {task}: {source}")]
    Json {
        /// The task being attempted.
        task: String,
        /// The original error being propagated.
        #[source]
        source: serde_json::Error,
    },

    /// Error emitted when failing to read an environment variable.
    #[error("Failed to get env var '{name}': {source}")]
    EnvVar {
        /// The name of the environment variable that was attempted to be read.
        name: String,
        /// The original error being propagated.
        #[source]
        source: std::env::VarError,
    },

    /// An error emitted when encountering an invalid [`OutputVariable`](crate::output_variable::OutputVariable).
    #[error("OutputVariable is malformed: {0}")]
    OutputVar(#[from] OutputVariableError),
}

impl RestClientError {
    /// Helper function to create an [`Self::EnvVar`] error with variable name and source error.
    pub fn env_var(name: &str, source: std::env::VarError) -> Self {
        Self::EnvVar {
            name: name.to_string(),
            source,
        }
    }

    /// Helper function to create an [`Self::Io`] error with task context.
    pub fn io(task: &str, source: std::io::Error) -> Self {
        Self::Io {
            task: task.to_string(),
            source,
        }
    }

    /// Builder function to add context to [`Self::Request`] errors.
    ///
    /// Returns a [`Self::RequestContext`] error if `self` is a [`Self::Request`] error.
    /// Otherwise, returns `self` unchanged.
    pub fn add_request_context(self, task: &str) -> Self {
        match self {
            Self::Request(e) => Self::RequestContext {
                task: task.to_string(),
                source: e,
            },
            _ => self,
        }
    }

    /// Helper function to create a [`Self::Json`] error with task context.
    pub fn json(task: &str, source: serde_json::Error) -> Self {
        Self::Json {
            task: task.to_string(),
            source,
        }
    }
}

/// The possible errors emitted by file system operations.
///
/// This is only used (via [`FileFilter::walk()`](fn@crate::file_utils::file_filter::FileFilter::walk_dir));
/// typically when not running within a supported CI environment.
#[cfg(feature = "file-changes")]
#[derive(Debug, Error)]
#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
pub enum DirWalkError {
    /// Error emitted when failing to read a directory entry.
    #[error("Failed to read {path}: {source}")]
    ReadDir {
        /// The path that was attempted to be read.
        path: PathBuf,
        /// The original error being propagated.
        #[source]
        source: std::io::Error,
    },

    /// Error emitted when failing to interact with files.
    #[error(transparent)]
    OsError(#[from] std::io::Error),
}

#[cfg(test)]
mod tests {
    use super::RestClientError;

    #[test]
    fn no_added_req_ctx() {
        let err = RestClientError::CannotCloneRequest;
        assert!(matches!(
            err.add_request_context("some task"),
            RestClientError::CannotCloneRequest
        ));
    }
}