rust_web_server/validate/mod.rs
1#[cfg(test)]
2mod tests;
3
4use crate::core::New;
5use crate::extract::FromRequest;
6use crate::mime_type::MimeType;
7use crate::range::Range;
8use crate::request::Request;
9use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
10
11/// A single field that failed validation.
12#[derive(Debug, Clone)]
13pub struct FieldError {
14 pub field: String,
15 pub message: String,
16}
17
18/// Collection of per-field validation errors returned by [`Validate::validate`].
19///
20/// Build one inside a manual `Validate` impl and return it as `Err(errors)` if
21/// `!errors.is_empty()`.
22#[derive(Debug)]
23pub struct ValidationErrors {
24 errors: Vec<FieldError>,
25}
26
27impl ValidationErrors {
28 pub fn new() -> Self {
29 Self { errors: Vec::new() }
30 }
31
32 /// Record a validation failure for `field` with a human-readable `message`.
33 pub fn add(&mut self, field: &str, message: &str) {
34 self.errors.push(FieldError {
35 field: field.to_string(),
36 message: message.to_string(),
37 });
38 }
39
40 pub fn is_empty(&self) -> bool {
41 self.errors.is_empty()
42 }
43
44 pub fn errors(&self) -> &[FieldError] {
45 &self.errors
46 }
47
48 /// Serialise as `{"errors":[{"field":"…","message":"…"},…]}`.
49 pub fn into_json(&self) -> String {
50 let entries: Vec<String> = self
51 .errors
52 .iter()
53 .map(|e| {
54 format!(
55 "{{\"field\":\"{}\",\"message\":\"{}\"}}",
56 escape(&e.field),
57 escape(&e.message),
58 )
59 })
60 .collect();
61 format!("{{\"errors\":[{}]}}", entries.join(","))
62 }
63}
64
65fn escape(s: &str) -> String {
66 s.replace('\\', "\\\\").replace('"', "\\\"")
67}
68
69/// Types that can be validated field-by-field.
70///
71/// Implement manually or derive with `#[derive(Validate)]`
72/// (requires `features = ["macros"]`).
73///
74/// # Example — manual implementation
75///
76/// ```rust,no_run
77/// use rust_web_server::validate::{Validate, ValidationErrors};
78///
79/// struct Payload { name: String }
80///
81/// impl Validate for Payload {
82/// fn validate(&self) -> Result<(), ValidationErrors> {
83/// let mut errors = ValidationErrors::new();
84/// if self.name.is_empty() { errors.add("name", "must not be empty"); }
85/// if errors.is_empty() { Ok(()) } else { Err(errors) }
86/// }
87/// }
88/// ```
89///
90/// # Example — derive macro
91///
92/// ```ignore
93/// use rust_web_server::validate::Validate;
94///
95/// #[derive(rust_web_server::Validate)]
96/// struct CreateUser {
97/// #[validate(length(min = 1, max = 50))]
98/// name: String,
99/// #[validate(email)]
100/// email: String,
101/// #[validate(range(min = 0, max = 150))]
102/// age: u8,
103/// }
104/// ```
105///
106/// Supported validators: `email`, `required`, `url`,
107/// `length(min = N, max = N)`, `range(min = N, max = N)`.
108pub trait Validate {
109 fn validate(&self) -> Result<(), ValidationErrors>;
110}
111
112/// Wraps a `T: FromRequest + Validate`, extracting and validating in one step.
113///
114/// Returns `422 Unprocessable Entity` with a JSON error body if validation
115/// fails, or the upstream extraction error (typically 400) if extraction fails.
116///
117/// # Example
118///
119/// ```rust,no_run
120/// use rust_web_server::validate::{Validate, Validated, ValidationErrors};
121/// use rust_web_server::extract::{FromRequest, BodyText};
122/// use rust_web_server::core::New;
123/// use rust_web_server::request::Request;
124/// use rust_web_server::response::Response;
125///
126/// struct Name(String);
127///
128/// impl FromRequest for Name {
129/// fn from_request(req: &Request) -> Result<Self, Response> {
130/// let BodyText(s) = BodyText::from_request(req)?;
131/// Ok(Name(s))
132/// }
133/// }
134///
135/// impl Validate for Name {
136/// fn validate(&self) -> Result<(), ValidationErrors> {
137/// let mut errors = ValidationErrors::new();
138/// if self.0.is_empty() { errors.add("name", "must not be empty"); }
139/// if errors.is_empty() { Ok(()) } else { Err(errors) }
140/// }
141/// }
142///
143/// fn handle(req: &Request) -> Response {
144/// let Validated(name) = match Validated::<Name>::from_request(req) {
145/// Ok(v) => v,
146/// Err(res) => return res, // 400 or 422
147/// };
148/// Response::new()
149/// }
150/// ```
151pub struct Validated<T>(pub T);
152
153impl<T: FromRequest + Validate> FromRequest for Validated<T> {
154 fn from_request(request: &Request) -> Result<Self, Response> {
155 let value = T::from_request(request)?;
156 value.validate().map_err(validation_error_response)?;
157 Ok(Validated(value))
158 }
159}
160
161fn validation_error_response(errors: ValidationErrors) -> Response {
162 let json = errors.into_json();
163 let cr = Range::get_content_range(json.into_bytes(), MimeType::APPLICATION_JSON.to_string());
164 let mut response = Response::new();
165 response.status_code = *STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity.status_code;
166 response.reason_phrase = STATUS_CODE_REASON_PHRASE
167 .n422_unprocessable_entity
168 .reason_phrase
169 .to_string();
170 response.content_range_list = vec![cr];
171 response
172}
173
174/// Returns `true` if `s` is a plausible email address.
175///
176/// Checks for a non-empty local part, exactly one `@`, and a domain that
177/// contains at least one `.` and does not start or end with `.`.
178pub fn is_email(s: &str) -> bool {
179 let mut parts = s.splitn(2, '@');
180 let local = parts.next().unwrap_or("");
181 let domain = match parts.next() {
182 Some(d) => d,
183 None => return false,
184 };
185 !local.is_empty()
186 && domain.len() >= 3
187 && domain.contains('.')
188 && !domain.starts_with('.')
189 && !domain.ends_with('.')
190}
191
192/// Returns `true` if `s` starts with `http://` or `https://`.
193pub fn is_url(s: &str) -> bool {
194 s.starts_with("http://") || s.starts_with("https://")
195}