1#![deny(missing_docs)]
2
3use std::ops::{Deref, DerefMut};
6
7use axum::{
8 Json,
9 extract::{FromRequest, Request, rejection::JsonRejection},
10 response::IntoResponse,
11};
12use garde::Validate;
13use http::StatusCode;
14use serde::{Serialize, de::DeserializeOwned};
15
16pub trait Pipe<Input>: Send + Sync + 'static {
18 type Output;
20
21 type Error: std::error::Error + Send + Sync + 'static;
23
24 fn transform(&self, input: Input) -> std::result::Result<Self::Output, Self::Error>;
26}
27
28#[derive(Clone, Debug, Default)]
30pub struct ValidationPipe;
31
32impl ValidationPipe {
33 pub fn new() -> Self {
35 Self
36 }
37
38 pub fn transform<T>(&self, input: T) -> Result<T>
40 where
41 T: Validate<Context = ()>,
42 {
43 input.validate().map_err(ValidationPipeError::Validation)?;
44 Ok(input)
45 }
46}
47
48impl<T> Pipe<T> for ValidationPipe
49where
50 T: Validate<Context = ()>,
51{
52 type Output = T;
53 type Error = ValidationPipeError;
54
55 fn transform(&self, input: T) -> std::result::Result<Self::Output, Self::Error> {
56 ValidationPipe::transform(self, input)
57 }
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct ValidatedJson<T>(pub T);
67
68impl<T> ValidatedJson<T> {
69 pub fn into_inner(self) -> T {
71 self.0
72 }
73}
74
75impl<T> Deref for ValidatedJson<T> {
76 type Target = T;
77
78 fn deref(&self) -> &Self::Target {
79 &self.0
80 }
81}
82
83impl<T> DerefMut for ValidatedJson<T> {
84 fn deref_mut(&mut self) -> &mut Self::Target {
85 &mut self.0
86 }
87}
88
89impl<S, T> FromRequest<S> for ValidatedJson<T>
90where
91 S: Send + Sync,
92 T: DeserializeOwned + Validate<Context = ()>,
93{
94 type Rejection = ValidatedJsonRejection;
95
96 async fn from_request(req: Request, state: &S) -> std::result::Result<Self, Self::Rejection> {
97 let Json(value) = Json::<T>::from_request(req, state)
98 .await
99 .map_err(ValidatedJsonRejection::Json)?;
100 let value = ValidationPipe::new()
101 .transform(value)
102 .map_err(ValidatedJsonRejection::Validation)?;
103 Ok(Self(value))
104 }
105}
106
107#[derive(Debug, thiserror::Error)]
109pub enum ValidatedJsonRejection {
110 #[error(transparent)]
112 Json(#[from] JsonRejection),
113 #[error(transparent)]
115 Validation(#[from] ValidationPipeError),
116}
117
118impl IntoResponse for ValidatedJsonRejection {
119 fn into_response(self) -> axum::response::Response {
120 match self {
121 Self::Json(error) => error.into_response(),
122 Self::Validation(error) => error.into_response(),
123 }
124 }
125}
126
127pub type Result<T> = std::result::Result<T, ValidationPipeError>;
129
130#[derive(Debug, thiserror::Error)]
132pub enum ValidationPipeError {
133 #[error("validation failed: {0}")]
135 Validation(#[from] garde::Report),
136}
137
138impl ValidationPipeError {
139 pub fn status_code(&self) -> StatusCode {
141 StatusCode::UNPROCESSABLE_ENTITY
142 }
143
144 pub fn code(&self) -> &'static str {
146 "validation_failed"
147 }
148
149 pub fn field_errors(&self) -> Vec<FieldValidationError> {
151 match self {
152 Self::Validation(errors) => {
153 let mut field_errors = errors
154 .iter()
155 .map(|(path, error)| field_error(&path.to_string(), error.message()))
156 .collect::<Vec<_>>();
157 field_errors.sort_by(|left, right| {
158 left.field
159 .cmp(&right.field)
160 .then_with(|| left.code.cmp(&right.code))
161 });
162 field_errors
163 }
164 }
165 }
166}
167
168fn field_error(field: &str, message: &str) -> FieldValidationError {
169 FieldValidationError {
170 field: field.to_owned(),
171 code: validation_code(message).to_owned(),
172 message: Some(message.to_owned()),
173 }
174}
175
176fn validation_code(message: &str) -> &'static str {
177 if message.starts_with("not a valid email") {
178 "email"
179 } else if message.starts_with("length is ") {
180 "length"
181 } else if message.starts_with("not a valid url") {
182 "url"
183 } else if message.starts_with("not a valid IP") || message.starts_with("not a valid IPv") {
184 "ip"
185 } else if message.starts_with("lower than ") || message.starts_with("greater than ") {
186 "range"
187 } else {
188 "invalid"
189 }
190}
191
192impl IntoResponse for ValidationPipeError {
193 fn into_response(self) -> axum::response::Response {
194 let status = self.status_code();
195 let body = Json(ValidationErrorBody {
196 error: ValidationErrorDetails {
197 code: self.code(),
198 message: "request validation failed",
199 fields: self.field_errors(),
200 },
201 });
202 (status, body).into_response()
203 }
204}
205
206#[derive(Debug, Serialize)]
207struct ValidationErrorBody {
208 error: ValidationErrorDetails,
209}
210
211#[derive(Debug, Serialize)]
212struct ValidationErrorDetails {
213 code: &'static str,
214 message: &'static str,
215 fields: Vec<FieldValidationError>,
216}
217
218#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
220pub struct FieldValidationError {
221 field: String,
222 code: String,
223 message: Option<String>,
224}
225
226impl FieldValidationError {
227 pub fn field(&self) -> &str {
229 &self.field
230 }
231
232 pub fn code(&self) -> &str {
234 &self.code
235 }
236
237 pub fn message(&self) -> Option<&str> {
239 self.message.as_deref()
240 }
241}