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