Skip to main content

solid_pod_rs_forge/
error.rs

1//! Typed error enum with HTTP status mapping, mirroring the shape of
2//! [`solid_pod_rs_git::error::GitError`].
3
4use thiserror::Error;
5
6/// All failure modes surfaced by the forge service.
7#[derive(Debug, Error)]
8pub enum ForgeError {
9    /// Resource does not exist (404).
10    #[error("not found: {0}")]
11    NotFound(String),
12
13    /// Malformed request / invalid parameter (400).
14    #[error("bad request: {0}")]
15    BadRequest(String),
16
17    /// Path traversal or otherwise unsafe path (400).
18    #[error("invalid path: {0}")]
19    PathTraversal(String),
20
21    /// Authentication required but missing/invalid (401).
22    #[error("unauthorised: {0}")]
23    Unauthorised(String),
24
25    /// Authenticated but not permitted — e.g. namespace-write guard (403).
26    #[error("forbidden: {0}")]
27    Forbidden(String),
28
29    /// Compare-and-swap failure: the base ref moved under a merge (409).
30    #[error("conflict: {0}")]
31    Conflict(String),
32
33    /// A required host capability (git version, CGI binary) is absent
34    /// or the feature is not compiled (501).
35    #[error("unsupported: {0}")]
36    Unsupported(String),
37
38    /// Underlying git porcelain/CGI error — status mapped from the inner
39    /// [`solid_pod_rs_git::error::GitError`].
40    #[error("git: {0}")]
41    Git(#[from] solid_pod_rs_git::error::GitError),
42
43    /// Filesystem or process I/O failure (500).
44    #[error("i/o: {0}")]
45    Io(#[from] std::io::Error),
46
47    /// JSON (de)serialisation failure for the spine (500).
48    #[error("serialisation: {0}")]
49    Serde(#[from] serde_json::Error),
50
51    /// A shared resource is held by another request; retry shortly (503).
52    #[error("busy: {0}")]
53    Busy(String),
54
55    /// A generic backend failure (500).
56    #[error("backend: {0}")]
57    Backend(String),
58}
59
60impl ForgeError {
61    /// Recommended HTTP status code.
62    #[must_use]
63    pub fn status_code(&self) -> u16 {
64        match self {
65            ForgeError::NotFound(_) => 404,
66            ForgeError::BadRequest(_) | ForgeError::PathTraversal(_) => 400,
67            ForgeError::Unauthorised(_) => 401,
68            ForgeError::Forbidden(_) => 403,
69            ForgeError::Conflict(_) => 409,
70            ForgeError::Unsupported(_) => 501,
71            ForgeError::Busy(_) => 503,
72            ForgeError::Git(g) => g.status_code(),
73            ForgeError::Io(_) | ForgeError::Serde(_) | ForgeError::Backend(_) => 500,
74        }
75    }
76
77    /// Render this error as a [`crate::ForgeResponse`] JSON body.
78    #[must_use]
79    pub fn to_response(&self) -> crate::ForgeResponse {
80        crate::ForgeResponse::error(self.status_code(), self.to_string())
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn status_mapping() {
90        assert_eq!(ForgeError::NotFound("x".into()).status_code(), 404);
91        assert_eq!(ForgeError::BadRequest("x".into()).status_code(), 400);
92        assert_eq!(ForgeError::PathTraversal("x".into()).status_code(), 400);
93        assert_eq!(ForgeError::Unauthorised("x".into()).status_code(), 401);
94        assert_eq!(ForgeError::Forbidden("x".into()).status_code(), 403);
95        assert_eq!(ForgeError::Conflict("x".into()).status_code(), 409);
96        assert_eq!(ForgeError::Unsupported("x".into()).status_code(), 501);
97        assert_eq!(ForgeError::Backend("x".into()).status_code(), 500);
98    }
99
100    #[test]
101    fn git_error_status_propagates() {
102        let g = solid_pod_rs_git::error::GitError::NotARepository("r".into());
103        let e = ForgeError::Git(g);
104        assert_eq!(e.status_code(), 404);
105    }
106}