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, thiserror::Error)]
26pub enum CoolError {
27 #[error("bad request: {0}")]
29 BadRequest(String),
30 #[error("not acceptable: {0}")]
31 NotAcceptable(String),
32 #[error("unauthorized: {0}")]
33 Unauthorized(String),
34 #[error("unsupported media type: {0}")]
35 UnsupportedMediaType(String),
36 #[error("forbidden: {0}")]
37 Forbidden(String),
38 #[error("not found: {0}")]
39 NotFound(String),
40 #[error("conflict: {0}")]
41 Conflict(String),
42 #[error("validation: {0}")]
43 Validation(String),
44 #[error("precondition failed: {0}")]
45 PreconditionFailed(String),
46 #[error("codec: {0}")]
49 Codec(String),
50 #[error("database: {0}")]
51 Database(String),
52 #[error("internal: {0}")]
53 Internal(String),
54}
55
56impl CoolError {
57 pub fn code(&self) -> &'static str {
58 match self {
59 Self::BadRequest(_) => "BAD_REQUEST",
60 Self::NotAcceptable(_) => "NOT_ACCEPTABLE",
61 Self::Unauthorized(_) => "UNAUTHORIZED",
62 Self::UnsupportedMediaType(_) => "UNSUPPORTED_MEDIA_TYPE",
63 Self::Forbidden(_) => "FORBIDDEN",
64 Self::NotFound(_) => "NOT_FOUND",
65 Self::Conflict(_) => "CONFLICT",
66 Self::Validation(_) => "VALIDATION_ERROR",
67 Self::PreconditionFailed(_) => "PRECONDITION_FAILED",
68 Self::Codec(_) => "CODEC_ERROR",
69 Self::Database(_) => "DATABASE_ERROR",
70 Self::Internal(_) => "INTERNAL_ERROR",
71 }
72 }
73
74 pub fn status_code(&self) -> StatusCode {
75 match self {
76 Self::BadRequest(_) => StatusCode::BAD_REQUEST,
77 Self::NotAcceptable(_) => StatusCode::NOT_ACCEPTABLE,
78 Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
79 Self::UnsupportedMediaType(_) => StatusCode::UNSUPPORTED_MEDIA_TYPE,
80 Self::Forbidden(_) => StatusCode::FORBIDDEN,
81 Self::NotFound(_) => StatusCode::NOT_FOUND,
82 Self::Conflict(_) => StatusCode::CONFLICT,
83 Self::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
84 Self::PreconditionFailed(_) => StatusCode::PRECONDITION_FAILED,
85 Self::Codec(_) => StatusCode::BAD_REQUEST,
86 Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
87 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
88 }
89 }
90
91 pub fn public_message(&self) -> Cow<'_, str> {
97 match self {
98 Self::BadRequest(s)
99 | Self::NotAcceptable(s)
100 | Self::Unauthorized(s)
101 | Self::UnsupportedMediaType(s)
102 | Self::Forbidden(s)
103 | Self::NotFound(s)
104 | Self::Conflict(s)
105 | Self::Validation(s)
106 | Self::PreconditionFailed(s) => Cow::Borrowed(s.as_str()),
107 Self::Codec(_) => Cow::Borrowed("invalid request payload"),
108 Self::Database(_) => Cow::Borrowed("internal error"),
109 Self::Internal(_) => Cow::Borrowed("internal error"),
110 }
111 }
112
113 pub fn detail(&self) -> Option<&str> {
118 match self {
119 Self::BadRequest(s)
120 | Self::NotAcceptable(s)
121 | Self::Unauthorized(s)
122 | Self::UnsupportedMediaType(s)
123 | Self::Forbidden(s)
124 | Self::NotFound(s)
125 | Self::Conflict(s)
126 | Self::Validation(s)
127 | Self::PreconditionFailed(s)
128 | Self::Codec(s)
129 | Self::Database(s)
130 | Self::Internal(s) => {
131 if s.is_empty() {
132 None
133 } else {
134 Some(s.as_str())
135 }
136 }
137 }
138 }
139
140 pub fn into_response(self) -> CoolErrorResponse {
141 let code = self.code().to_owned();
142 let message = self.public_message().into_owned();
143 CoolErrorResponse {
144 code,
145 message,
146 details: None,
147 }
148 }
149}
150
151pub fn parse_cuid(value: &str) -> Result<String, CoolError> {
152 if is_valid_cuid(value) {
153 Ok(value.to_owned())
154 } else {
155 Err(CoolError::BadRequest(format!(
156 "invalid cuid '{}': expected a lowercase alphanumeric id starting with 'c'",
157 value,
158 )))
159 }
160}
161
162fn is_valid_cuid(value: &str) -> bool {
163 let mut chars = value.chars();
164 let Some(first) = chars.next() else {
165 return false;
166 };
167 if first != 'c' || value.len() < 2 {
168 return false;
169 }
170 chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
171}