Skip to content

Commit 1c00cdb

Browse files
committed
Rough prototype of extending CustomApiError for multiple errors.
1 parent 87621e0 commit 1c00cdb

File tree

3 files changed

+99
-10
lines changed

3 files changed

+99
-10
lines changed

src/controllers/krate/delete.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::app::AppState;
22
use crate::auth::AuthCheck;
33
use crate::models::{Crate, Rights};
44
use crate::schema::{crate_downloads, crates, dependencies};
5-
use crate::util::errors::{crate_not_found, custom, AppResult, BoxedAppError};
5+
use crate::util::errors::{crate_not_found, custom, AppResult, BoxedAppError, CustomApiError};
66
use crate::worker::jobs;
77
use axum::extract::Path;
88
use bigdecimal::ToPrimitive;
@@ -56,9 +56,10 @@ pub async fn delete(Path(name): Path<String>, parts: Parts, app: AppState) -> Ap
5656

5757
let is_old = created_at <= Utc::now() - chrono::Duration::hours(72);
5858
if is_old {
59+
let mut errors = CustomApiError::new(StatusCode::UNPROCESSABLE_ENTITY);
60+
5961
if owners.len() > 1 {
60-
let msg = "only crates with a single owner can be deleted after 72 hours";
61-
return Err(custom(StatusCode::UNPROCESSABLE_ENTITY, msg));
62+
errors.push("only crates with a single owner can be deleted after 72 hours");
6263
}
6364

6465
let age = Utc::now().signed_duration_since(created_at);
@@ -78,7 +79,7 @@ pub async fn delete(Path(name): Path<String>, parts: Parts, app: AppState) -> Ap
7879

7980
if downloads > max_downloads {
8081
let msg = format!("only crates with less than {DOWNLOADS_PER_MONTH_LIMIT} downloads per month can be deleted after 72 hours");
81-
return Err(custom(StatusCode::UNPROCESSABLE_ENTITY, msg));
82+
errors.push(msg);
8283
}
8384

8485
let has_rev_dep = dependencies::table
@@ -90,8 +91,11 @@ pub async fn delete(Path(name): Path<String>, parts: Parts, app: AppState) -> Ap
9091
.is_some();
9192

9293
if has_rev_dep {
93-
let msg = "only crates without reverse dependencies can be deleted after 72 hours";
94-
return Err(custom(StatusCode::UNPROCESSABLE_ENTITY, msg));
94+
errors.push("only crates without reverse dependencies can be deleted after 72 hours");
95+
}
96+
97+
if errors.contains_errors() {
98+
return Err(errors.into());
9599
}
96100
}
97101

src/util/errors.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ mod json;
3232
use crate::email::EmailError;
3333
use crates_io_github::GitHubError;
3434
pub use json::TOKEN_FORMAT_ERROR;
35-
pub(crate) use json::{custom, InsecurelyGeneratedTokenRevoked, ReadOnlyMode, TooManyRequests};
35+
pub(crate) use json::{
36+
custom, CustomApiError, InsecurelyGeneratedTokenRevoked, ReadOnlyMode, TooManyRequests,
37+
};
3638

3739
pub type BoxedAppError = Box<dyn AppError>;
3840

src/util/errors/json.rs

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,71 @@ impl fmt::Display for ReadOnlyMode {
4040
pub fn custom(status: StatusCode, detail: impl Into<Cow<'static, str>>) -> BoxedAppError {
4141
Box::new(CustomApiError {
4242
status,
43-
detail: detail.into(),
43+
detail: Detail::Single(detail.into()),
4444
})
4545
}
4646

4747
#[derive(Debug, Clone)]
4848
pub struct CustomApiError {
4949
status: StatusCode,
50-
detail: Cow<'static, str>,
50+
detail: Detail,
51+
}
52+
53+
#[derive(Debug, Clone)]
54+
enum Detail {
55+
Empty,
56+
Single(Cow<'static, str>),
57+
Multiple(Vec<Cow<'static, str>>),
58+
}
59+
60+
impl fmt::Display for Detail {
61+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62+
match self {
63+
Self::Empty => write!(f, ""),
64+
Self::Single(msg) => write!(f, "{msg}"),
65+
Self::Multiple(msgs) => write!(f, "{}", msgs.join(", ")),
66+
}
67+
}
68+
}
69+
70+
impl CustomApiError {
71+
pub fn new(status: StatusCode) -> Self {
72+
Self {
73+
status,
74+
detail: Detail::Empty,
75+
}
76+
}
77+
78+
pub fn contains_errors(&self) -> bool {
79+
!self.is_empty()
80+
}
81+
82+
pub fn is_empty(&self) -> bool {
83+
matches!(&self.detail, Detail::Empty)
84+
}
85+
86+
pub fn push(&mut self, detail: impl Into<Cow<'static, str>>) -> &mut Self {
87+
match &mut self.detail {
88+
Detail::Empty => {
89+
self.detail = Detail::Single(detail.into());
90+
}
91+
Detail::Single(msg) => {
92+
let msg = msg.clone();
93+
self.detail = Detail::Multiple(vec![msg, detail.into()]);
94+
}
95+
Detail::Multiple(msgs) => {
96+
msgs.push(detail.into());
97+
}
98+
}
99+
100+
self
101+
}
102+
}
103+
104+
impl From<CustomApiError> for BoxedAppError {
105+
fn from(value: CustomApiError) -> Self {
106+
Box::new(value)
107+
}
51108
}
52109

53110
impl fmt::Display for CustomApiError {
@@ -58,7 +115,33 @@ impl fmt::Display for CustomApiError {
58115

59116
impl AppError for CustomApiError {
60117
fn response(&self) -> Response {
61-
json_error(&self.detail, self.status)
118+
#[derive(Serialize)]
119+
struct ErrorContent {
120+
detail: Cow<'static, str>,
121+
}
122+
123+
impl From<&Cow<'static, str>> for ErrorContent {
124+
fn from(value: &Cow<'static, str>) -> Self {
125+
Self {
126+
detail: value.clone(),
127+
}
128+
}
129+
}
130+
131+
#[derive(Serialize)]
132+
struct ErrorBody {
133+
errors: Vec<ErrorContent>,
134+
}
135+
136+
let body = ErrorBody {
137+
errors: match &self.detail {
138+
Detail::Empty => Vec::new(),
139+
Detail::Single(msg) => vec![msg.into()],
140+
Detail::Multiple(msgs) => msgs.iter().map(|msg| msg.into()).collect(),
141+
},
142+
};
143+
144+
(self.status, Json(body)).into_response()
62145
}
63146
}
64147

0 commit comments

Comments
 (0)