Skip to main content

git_bot_feedback/
error.rs

1//! Error types used across the git-bot-feedback crate.
2#[cfg(feature = "pyo3")]
3use pyo3::{
4    exceptions::{PyOSError, PyRuntimeError, PyValueError},
5    prelude::*,
6};
7
8#[cfg(feature = "file-changes")]
9use std::path::PathBuf;
10
11use chrono::{DateTime, Utc};
12use thiserror::Error;
13
14use crate::client::MAX_RETRIES;
15
16/// The possible errors emitted when parsing git diffs.
17#[derive(Debug, thiserror::Error)]
18#[cfg(feature = "file-changes")]
19#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
20pub enum DiffError {
21    /// An error emitted when failing to compile a Regular expression pattern.
22    #[error("Failed to compile regex pattern: {0}")]
23    RegExCompileFailed(#[from] regex::Error),
24}
25
26/// The possible errors emitted when validating an [`OutputVariable`](struct@crate::OutputVariable).
27#[derive(Debug, thiserror::Error, PartialEq, Eq)]
28pub enum OutputVariableError {
29    /// The output variable's name is empty.
30    #[error("The output variable's name is empty")]
31    NameIsEmpty,
32    /// The output variable's name starts with a number.
33    #[error("The output variable's name starts with a number: '{0}'")]
34    NameStartsWithNumber(String),
35    /// The output variable's name contains non-printable characters.
36    #[error("The output variable's name contains non-printable characters: '{0}'")]
37    NameContainsNonPrintableCharacters(String),
38    /// The output variable's value contains non-printable characters.
39    #[error("The output variable's value contains non-printable characters: '{0}'")]
40    ValueContainsNonPrintableCharacters(String),
41    /// Unsupported CI platform.
42    #[error("Unsupported CI platform")]
43    UnsupportedPlatform,
44}
45
46/// The possible error emitted by the REST client API
47#[derive(Debug, Error)]
48pub enum RestClientError {
49    /// Errors related to parsing git diffs.
50    #[error(transparent)]
51    #[cfg(feature = "file-changes")]
52    #[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
53    DiffError(#[from] DiffError),
54
55    /// Error emitted when encountering malformed event information.
56    #[error("Encountered malformed event info: {0}")]
57    MalformedEventInfo(String),
58
59    /// Error related to making HTTP requests
60    #[error(transparent)]
61    Request(#[from] reqwest::Error),
62
63    /// Error related to making HTTP requests, with additional context about the request that caused the error.
64    #[error("Failed to {task}: {source}")]
65    RequestContext {
66        /// The task being attempted.
67        task: String,
68        /// The original error being propagated.
69        #[source]
70        source: reqwest::Error,
71    },
72
73    /// Errors related to standard I/O.
74    #[error("Failed to {task}: {source}")]
75    Io {
76        /// The task being attempted.
77        task: String,
78        /// The original error being propagated.
79        #[source]
80        source: std::io::Error,
81    },
82
83    /// Error related to `git` command execution.
84    #[error("Git command error: {0}")]
85    #[cfg(feature = "file-changes")]
86    #[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
87    GitCommand(String),
88
89    /// Error related to exceeding REST API Rate limits and
90    /// no reset time is provided in the response headers.
91    #[error("Primary Rate Limit exceeded (no reset time provided)")]
92    RateLimitNoReset,
93
94    /// Error related to exceeding REST API Rate limits with a known reset time.
95    #[error("Primary Rate Limit exceeded; resets at {0}")]
96    RateLimitPrimary(DateTime<Utc>),
97
98    /// Error related to exhausting all retries after hitting REST API Rate limits.
99    #[error("Rate Limit exceeded after all {MAX_RETRIES} retries exhausted")]
100    RateLimitSecondary,
101
102    /// Error emitted when failing to clone a request object.
103    #[error("Failed to clone request object for auto-retries")]
104    CannotCloneRequest,
105
106    /// Error emitted when failing to create a header value.
107    #[error("Tried to create a header value from invalid string data")]
108    InvalidHeaderValue(#[from] reqwest::header::InvalidHeaderValue),
109
110    /// Error emitted when failing to convert a header value to string.
111    #[error("Failed to convert header value to string")]
112    UnexpectedHeaderValue(#[from] reqwest::header::ToStrError),
113
114    /// Error emitted when failing to parse an integer from a header value (as a UTF-8 string).
115    #[error("Failed to parse integer from header value: {0}")]
116    HeaderParseInt(#[from] std::num::ParseIntError),
117
118    /// Error emitted when failing to parse a URL.
119    #[error("Failed to parse URL:{0}")]
120    UrlParse(#[from] url::ParseError),
121
122    /// Error emitted when failing to deserialize/serialize request/response JSON data.
123    #[error("Failed to {task}: {source}")]
124    Json {
125        /// The task being attempted.
126        task: String,
127        /// The original error being propagated.
128        #[source]
129        source: serde_json::Error,
130    },
131
132    /// Error emitted when failing to read an environment variable.
133    #[error("Failed to get env var '{name}': {source}")]
134    EnvVar {
135        /// The name of the environment variable that was attempted to be read.
136        name: String,
137        /// The original error being propagated.
138        #[source]
139        source: std::env::VarError,
140    },
141
142    /// An error emitted when encountering an invalid [`OutputVariable`](crate::output_variable::OutputVariable).
143    #[error("OutputVariable is malformed: {0}")]
144    OutputVar(#[from] OutputVariableError),
145}
146
147impl RestClientError {
148    /// Helper function to create an [`Self::EnvVar`] error with variable name and source error.
149    pub fn env_var(name: &str, source: std::env::VarError) -> Self {
150        Self::EnvVar {
151            name: name.to_string(),
152            source,
153        }
154    }
155
156    /// Helper function to create an [`Self::Io`] error with task context.
157    pub fn io(task: &str, source: std::io::Error) -> Self {
158        Self::Io {
159            task: task.to_string(),
160            source,
161        }
162    }
163
164    /// Builder function to add context to [`Self::Request`] errors.
165    ///
166    /// Returns a [`Self::RequestContext`] error if `self` is a [`Self::Request`] error.
167    /// Otherwise, returns `self` unchanged.
168    pub fn add_request_context(self, task: &str) -> Self {
169        match self {
170            Self::Request(e) => Self::RequestContext {
171                task: task.to_string(),
172                source: e,
173            },
174            _ => self,
175        }
176    }
177
178    /// Helper function to create a [`Self::Json`] error with task context.
179    pub fn json(task: &str, source: serde_json::Error) -> Self {
180        Self::Json {
181            task: task.to_string(),
182            source,
183        }
184    }
185}
186
187/// The possible errors emitted by file system operations.
188///
189/// This is only used (via [`FileFilter::walk()`](fn@crate::file_utils::file_filter::FileFilter::walk_dir));
190/// typically when not running within a supported CI environment.
191#[cfg(feature = "file-changes")]
192#[derive(Debug, Error)]
193#[cfg_attr(docsrs, doc(cfg(feature = "file-changes")))]
194pub enum DirWalkError {
195    /// Error emitted when failing to read a directory entry.
196    #[error("Failed to read {path}: {source}")]
197    ReadDir {
198        /// The path that was attempted to be read.
199        path: PathBuf,
200        /// The original error being propagated.
201        #[source]
202        source: std::io::Error,
203    },
204
205    /// Error emitted when failing to interact with files.
206    #[error(transparent)]
207    OsError(#[from] std::io::Error),
208}
209
210#[cfg(feature = "pyo3")]
211impl From<OutputVariableError> for PyErr {
212    fn from(e: OutputVariableError) -> Self {
213        match e {
214            OutputVariableError::NameIsEmpty
215            | OutputVariableError::NameStartsWithNumber(_)
216            | OutputVariableError::NameContainsNonPrintableCharacters(_)
217            | OutputVariableError::ValueContainsNonPrintableCharacters(_) => {
218                PyValueError::new_err(format!("{e:?}"))
219            }
220            OutputVariableError::UnsupportedPlatform => PyRuntimeError::new_err(format!("{e:?}")),
221        }
222    }
223}
224
225#[cfg(feature = "pyo3")]
226impl From<DiffError> for PyErr {
227    fn from(e: DiffError) -> Self {
228        match e {
229            DiffError::RegExCompileFailed(_) => PyValueError::new_err(format!("{e:?}")),
230        }
231    }
232}
233
234#[cfg(feature = "pyo3")]
235impl From<DirWalkError> for PyErr {
236    fn from(e: DirWalkError) -> Self {
237        PyOSError::new_err(format!("{e:?}"))
238    }
239}
240
241#[cfg(feature = "pyo3")]
242impl From<RestClientError> for PyErr {
243    fn from(err: RestClientError) -> Self {
244        match err {
245            #[cfg(feature = "file-changes")]
246            RestClientError::DiffError(e) => e.into(),
247            RestClientError::MalformedEventInfo(_) => PyRuntimeError::new_err(format!("{err:?}")),
248            RestClientError::Request(e) => PyOSError::new_err(format!("{e:?}")),
249            RestClientError::RequestContext { task: _, source: _ }
250            | RestClientError::Io { task: _, source: _ }
251            | RestClientError::RateLimitNoReset
252            | RestClientError::RateLimitPrimary(_)
253            | RestClientError::RateLimitSecondary => PyOSError::new_err(format!("{err:?}")),
254            RestClientError::CannotCloneRequest
255            | RestClientError::InvalidHeaderValue(_)
256            | RestClientError::UnexpectedHeaderValue(_)
257            | RestClientError::HeaderParseInt(_)
258            | RestClientError::UrlParse(_)
259            | RestClientError::Json { task: _, source: _ }
260            | RestClientError::EnvVar { name: _, source: _ } => {
261                PyValueError::new_err(format!("{err:?}"))
262            }
263            #[cfg(feature = "file-changes")]
264            RestClientError::GitCommand(_) => PyValueError::new_err(format!("{err:?}")),
265            RestClientError::OutputVar(e) => e.into(),
266        }
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::RestClientError;
273
274    #[test]
275    fn no_added_req_ctx() {
276        let err = RestClientError::CannotCloneRequest;
277        assert!(matches!(
278            err.add_request_context("some task"),
279            RestClientError::CannotCloneRequest
280        ));
281    }
282}