Skip to main content

rustio_admin/
error.rs

1//! Unified error type. Every fallible path in the framework returns
2//! `Result<T, Error>`, and the HTTP layer knows how to turn an `Error`
3//! into a proper response.
4
5use std::fmt;
6
7// public:
8pub type Result<T> = std::result::Result<T, Error>;
9
10// public:
11#[derive(Debug)]
12pub enum Error {
13    BadRequest(String),
14    Unauthorized(String),
15    Forbidden(String),
16    NotFound(String),
17    MethodNotAllowed(String),
18    Conflict(String),
19    Internal(String),
20}
21
22impl Error {
23    // public:
24    pub fn status(&self) -> u16 {
25        match self {
26            Error::BadRequest(_) => 400,
27            Error::Unauthorized(_) => 401,
28            Error::Forbidden(_) => 403,
29            Error::NotFound(_) => 404,
30            Error::MethodNotAllowed(_) => 405,
31            Error::Conflict(_) => 409,
32            Error::Internal(_) => 500,
33        }
34    }
35
36    // public:
37    /// The message as shown to the client. For 500s we deliberately
38    /// return a generic string — the real detail stays in logs.
39    pub fn client_message(&self) -> &str {
40        match self {
41            Error::BadRequest(m)
42            | Error::Unauthorized(m)
43            | Error::Forbidden(m)
44            | Error::NotFound(m)
45            | Error::MethodNotAllowed(m)
46            | Error::Conflict(m) => m,
47            Error::Internal(_) => "Internal Server Error",
48        }
49    }
50}
51
52impl fmt::Display for Error {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        let label = match self {
55            Error::BadRequest(m) => format!("400 Bad Request: {m}"),
56            Error::Unauthorized(m) => format!("401 Unauthorized: {m}"),
57            Error::Forbidden(m) => format!("403 Forbidden: {m}"),
58            Error::NotFound(m) => format!("404 Not Found: {m}"),
59            Error::MethodNotAllowed(m) => format!("405 Method Not Allowed: {m}"),
60            Error::Conflict(m) => format!("409 Conflict: {m}"),
61            Error::Internal(m) => format!("500 Internal: {m}"),
62        };
63        f.write_str(&label)
64    }
65}
66
67impl std::error::Error for Error {}
68
69impl From<sqlx::Error> for Error {
70    fn from(e: sqlx::Error) -> Self {
71        match e {
72            sqlx::Error::RowNotFound => Error::NotFound("row not found".into()),
73            // Postgres constraint violations (FK / unique / NOT NULL /
74            // check) are user-input bugs, not internal failures. Surface
75            // them as 409 Conflict so callers can distinguish "the user
76            // picked a bad value" from "the DB is on fire".
77            sqlx::Error::Database(db_err) if db_err.constraint().is_some() => {
78                let constraint = db_err.constraint().unwrap_or("?").to_string();
79                Error::Conflict(format!("constraint violation ({constraint}): {db_err}"))
80            }
81            other => Error::Internal(format!("db error: {other}")),
82        }
83    }
84}
85
86impl From<std::io::Error> for Error {
87    fn from(e: std::io::Error) -> Self {
88        Error::Internal(format!("io error: {e}"))
89    }
90}
91
92impl From<serde_json::Error> for Error {
93    fn from(e: serde_json::Error) -> Self {
94        Error::BadRequest(format!("json error: {e}"))
95    }
96}
97
98impl From<minijinja::Error> for Error {
99    fn from(e: minijinja::Error) -> Self {
100        Error::Internal(format!("template error: {e}"))
101    }
102}