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 generic backend failure (500).
52    #[error("backend: {0}")]
53    Backend(String),
54}
55
56impl ForgeError {
57    /// Recommended HTTP status code.
58    #[must_use]
59    pub fn status_code(&self) -> u16 {
60        match self {
61            ForgeError::NotFound(_) => 404,
62            ForgeError::BadRequest(_) | ForgeError::PathTraversal(_) => 400,
63            ForgeError::Unauthorised(_) => 401,
64            ForgeError::Forbidden(_) => 403,
65            ForgeError::Conflict(_) => 409,
66            ForgeError::Unsupported(_) => 501,
67            ForgeError::Git(g) => g.status_code(),
68            ForgeError::Io(_) | ForgeError::Serde(_) | ForgeError::Backend(_) => 500,
69        }
70    }
71
72    /// Render this error as a [`crate::ForgeResponse`] JSON body.
73    #[must_use]
74    pub fn to_response(&self) -> crate::ForgeResponse {
75        crate::ForgeResponse::error(self.status_code(), self.to_string())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn status_mapping() {
85        assert_eq!(ForgeError::NotFound("x".into()).status_code(), 404);
86        assert_eq!(ForgeError::BadRequest("x".into()).status_code(), 400);
87        assert_eq!(ForgeError::PathTraversal("x".into()).status_code(), 400);
88        assert_eq!(ForgeError::Unauthorised("x".into()).status_code(), 401);
89        assert_eq!(ForgeError::Forbidden("x".into()).status_code(), 403);
90        assert_eq!(ForgeError::Conflict("x".into()).status_code(), 409);
91        assert_eq!(ForgeError::Unsupported("x".into()).status_code(), 501);
92        assert_eq!(ForgeError::Backend("x".into()).status_code(), 500);
93    }
94
95    #[test]
96    fn git_error_status_propagates() {
97        let g = solid_pod_rs_git::error::GitError::NotARepository("r".into());
98        let e = ForgeError::Git(g);
99        assert_eq!(e.status_code(), 404);
100    }
101}