1use axum::http::StatusCode;
9use axum::response::{IntoResponse, Response};
10use forge_jobs::StorageError;
11use serde::Serialize;
12
13#[derive(Debug, Serialize)]
14#[serde(tag = "kind", rename_all = "snake_case")]
15#[non_exhaustive]
16pub enum Error {
17 Validation { field: String, msg: String },
19 NotFound { msg: String },
21 Conflict { msg: String },
23 RateLimited { retry_after_secs: u32 },
25 Storage { msg: String },
27 Internal { msg: String },
29}
30
31impl Error {
32 #[must_use]
33 pub fn validation(field: impl Into<String>, msg: impl Into<String>) -> Self {
34 Self::Validation {
35 field: field.into(),
36 msg: msg.into(),
37 }
38 }
39
40 #[must_use]
41 pub fn not_found(msg: impl Into<String>) -> Self {
42 Self::NotFound { msg: msg.into() }
43 }
44
45 #[must_use]
46 pub fn internal(msg: impl Into<String>) -> Self {
47 Self::Internal { msg: msg.into() }
48 }
49}
50
51impl From<StorageError> for Error {
52 fn from(e: StorageError) -> Self {
53 match e {
54 StorageError::NotFound(msg) => Self::NotFound { msg },
55 StorageError::InvalidInput(msg) => Self::Validation {
56 field: "input".into(),
57 msg,
58 },
59 StorageError::Conflict(msg) => Self::Conflict { msg },
60 other if other.is_transient_conflict() => Self::Conflict {
64 msg: other.to_string(),
65 },
66 other => {
71 tracing::error!(error = %other, "storage backend error (500)");
72 Self::Storage {
73 msg: "storage backend error".to_owned(),
74 }
75 }
76 }
77 }
78}
79
80impl From<serde_json::Error> for Error {
81 fn from(e: serde_json::Error) -> Self {
82 Self::Validation {
83 field: "json".into(),
84 msg: e.to_string(),
85 }
86 }
87}
88
89impl std::fmt::Display for Error {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 Self::Validation { field, msg } => write!(f, "validation({field}): {msg}"),
93 Self::NotFound { msg } => write!(f, "not_found: {msg}"),
94 Self::Conflict { msg } => write!(f, "conflict: {msg}"),
95 Self::RateLimited { retry_after_secs } => {
96 write!(f, "rate_limited (retry in {retry_after_secs}s)")
97 }
98 Self::Storage { msg } => write!(f, "storage: {msg}"),
99 Self::Internal { msg } => write!(f, "internal: {msg}"),
100 }
101 }
102}
103
104impl std::error::Error for Error {}
105
106impl IntoResponse for Error {
107 fn into_response(self) -> Response {
108 let status = match &self {
109 Self::Validation { .. } => StatusCode::BAD_REQUEST,
110 Self::NotFound { .. } => StatusCode::NOT_FOUND,
111 Self::Conflict { .. } => StatusCode::CONFLICT,
112 Self::RateLimited { .. } => StatusCode::TOO_MANY_REQUESTS,
113 Self::Storage { .. } | Self::Internal { .. } => StatusCode::INTERNAL_SERVER_ERROR,
114 };
115 let body = axum::Json(&self);
116 (status, body).into_response()
117 }
118}