1use std::borrow::Cow;
9
10use http::StatusCode;
11use serde::{Deserialize, Serialize};
12
13use crate::value::Value;
14
15#[cfg(test)]
16mod tests;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct CoolErrorResponse {
20 pub code: String,
21 pub message: String,
22 pub details: Option<Value>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
34pub struct DbErrorInfo {
35 pub detail: String,
37 pub sqlstate: Option<String>,
40 pub constraint: Option<String>,
43}
44
45#[derive(Debug, thiserror::Error)]
46#[non_exhaustive]
47pub enum CoolError {
48 #[error("bad request: {0}")]
50 BadRequest(String),
51 #[error("not acceptable: {0}")]
52 NotAcceptable(String),
53 #[error("unauthorized: {0}")]
54 Unauthorized(String),
55 #[error("unsupported media type: {0}")]
56 UnsupportedMediaType(String),
57 #[error("forbidden: {0}")]
58 Forbidden(String),
59 #[error("not found: {0}")]
60 NotFound(String),
61 #[error("conflict: {0}")]
62 Conflict(String),
63 #[error("validation: {0}")]
64 Validation(String),
65 #[error("precondition failed: {0}")]
66 PreconditionFailed(String),
67 #[error("codec: {0}")]
70 Codec(String),
71 #[error("database: {0}")]
75 Database(String),
76 #[error("database: {}", .0.detail)]
81 DatabaseTyped(DbErrorInfo),
82 #[error("internal: {0}")]
83 Internal(String),
84}
85
86impl CoolError {
87 pub fn code(&self) -> &'static str {
88 match self {
89 Self::BadRequest(_) => "BAD_REQUEST",
90 Self::NotAcceptable(_) => "NOT_ACCEPTABLE",
91 Self::Unauthorized(_) => "UNAUTHORIZED",
92 Self::UnsupportedMediaType(_) => "UNSUPPORTED_MEDIA_TYPE",
93 Self::Forbidden(_) => "FORBIDDEN",
94 Self::NotFound(_) => "NOT_FOUND",
95 Self::Conflict(_) => "CONFLICT",
96 Self::Validation(_) => "VALIDATION_ERROR",
97 Self::PreconditionFailed(_) => "PRECONDITION_FAILED",
98 Self::Codec(_) => "CODEC_ERROR",
99 Self::Database(_) | Self::DatabaseTyped(_) => "DATABASE_ERROR",
100 Self::Internal(_) => "INTERNAL_ERROR",
101 }
102 }
103
104 pub fn status_code(&self) -> StatusCode {
105 match self {
106 Self::BadRequest(_) => StatusCode::BAD_REQUEST,
107 Self::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
108 Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
109 Self::UnsupportedMediaType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
110 Self::Forbidden(_) => StatusCode::FORBIDDEN,
111 Self::NotFound(_) => StatusCode::NOT_FOUND,
112 Self::Conflict(_) => StatusCode::CONFLICT,
113 Self::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
114 Self::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
115 Self::Codec(_) => StatusCode::BAD_REQUEST,
116 Self::Database(_) | Self::DatabaseTyped(_) => StatusCode::INTERNAL_SERVER_ERROR,
117 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
118 }
119 }
120
121 pub fn public_message(&self) -> Cow<'_, str> {
127 match self {
128 Self::BadRequest(s)
129 | Self::NotAcceptable(s)
130 | Self::Unauthorized(s)
131 | Self::UnsupportedMediaType(s)
132 | Self::Forbidden(s)
133 | Self::NotFound(s)
134 | Self::Conflict(s)
135 | Self::Validation(s)
136 | Self::PreconditionFailed(s) => Cow::Borrowed(s.as_str()),
137 Self::Codec(_) => Cow::Borrowed("invalid request payload"),
138 Self::Database(_) | Self::DatabaseTyped(_) => Cow::Borrowed("internal error"),
139 Self::Internal(_) => Cow::Borrowed("internal error"),
140 }
141 }
142
143 pub fn detail(&self) -> Option<&str> {
148 match self {
149 Self::BadRequest(s)
150 | Self::NotAcceptable(s)
151 | Self::Unauthorized(s)
152 | Self::UnsupportedMediaType(s)
153 | Self::Forbidden(s)
154 | Self::NotFound(s)
155 | Self::Conflict(s)
156 | Self::Validation(s)
157 | Self::PreconditionFailed(s)
158 | Self::Codec(s)
159 | Self::Database(s)
160 | Self::Internal(s) => {
161 if s.is_empty() {
162 None
163 } else {
164 Some(s.as_str())
165 }
166 }
167 Self::DatabaseTyped(info) => {
168 if info.detail.is_empty() {
169 None
170 } else {
171 Some(info.detail.as_str())
172 }
173 }
174 }
175 }
176
177 pub fn db_sqlstate(&self) -> Option<&str> {
184 match self {
185 Self::DatabaseTyped(info) => info.sqlstate.as_deref(),
186 _ => None,
187 }
188 }
189
190 pub fn db_constraint(&self) -> Option<&str> {
197 match self {
198 Self::DatabaseTyped(info) => info.constraint.as_deref(),
199 _ => None,
200 }
201 }
202
203 pub fn into_response(self) -> CoolErrorResponse {
204 let code = self.code().to_owned();
205 let message = self.public_message().into_owned();
206 CoolErrorResponse {
207 code,
208 message,
209 details: None,
210 }
211 }
212}
213
214pub fn parse_cuid(value: &str) -> Result<String, CoolError> {
215 if is_valid_cuid(value) {
216 Ok(value.to_owned())
217 } else {
218 Err(CoolError::BadRequest(format!(
219 "invalid cuid '{}': expected a lowercase alphanumeric id starting with 'c'",
220 value,
221 )))
222 }
223}
224
225fn is_valid_cuid(value: &str) -> bool {
226 let mut chars = value.chars();
227 let Some(first) = chars.next() else {
228 return false;
229 };
230 if first != 'c' || value.len() < 2 {
231 return false;
232 }
233 chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
234}