rws_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/// # extern crate rws_server as rust_web_server;
78/// use rust_web_server::validate::{Validate, ValidationErrors};
79///
80/// struct Payload { name: String }
81///
82/// impl Validate for Payload {
83/// fn validate(&self) -> Result<(), ValidationErrors> {
84/// let mut errors = ValidationErrors::new();
85/// if self.name.is_empty() { errors.add("name", "must not be empty"); }
86/// if errors.is_empty() { Ok(()) } else { Err(errors) }
87/// }
88/// }
89/// ```
90///
91/// # Example — derive macro
92///
93/// ```ignore
94/// use rust_web_server::validate::Validate;
95///
96/// #[derive(rust_web_server::Validate)]
97/// struct CreateUser {
98/// #[validate(length(min = 1, max = 50))]
99/// name: String,
100/// #[validate(email)]
101/// email: String,
102/// #[validate(range(min = 0, max = 150))]
103/// age: u8,
104/// }
105/// ```
106///
107/// Supported validators: `email`, `required`, `url`,
108/// `length(min = N, max = N)`, `range(min = N, max = N)`.
109pub trait Validate {
110 fn validate(&self) -> Result<(), ValidationErrors>;
111}
112
113/// Wraps a `T: FromRequest + Validate`, extracting and validating in one step.
114///
115/// Returns `422 Unprocessable Entity` with a JSON error body if validation
116/// fails, or the upstream extraction error (typically 400) if extraction fails.
117///
118/// # Example
119///
120/// ```rust,no_run
121/// # extern crate rws_server as rust_web_server;
122/// use rust_web_server::validate::{Validate, Validated, ValidationErrors};
123/// use rust_web_server::extract::{FromRequest, BodyText};
124/// use rust_web_server::core::New;
125/// use rust_web_server::request::Request;
126/// use rust_web_server::response::Response;
127///
128/// struct Name(String);
129///
130/// impl FromRequest for Name {
131/// fn from_request(req: &Request) -> Result<Self, Response> {
132/// let BodyText(s) = BodyText::from_request(req)?;
133/// Ok(Name(s))
134/// }
135/// }
136///
137/// impl Validate for Name {
138/// fn validate(&self) -> Result<(), ValidationErrors> {
139/// let mut errors = ValidationErrors::new();
140/// if self.0.is_empty() { errors.add("name", "must not be empty"); }
141/// if errors.is_empty() { Ok(()) } else { Err(errors) }
142/// }
143/// }
144///
145/// fn handle(req: &Request) -> Response {
146/// let Validated(name) = match Validated::<Name>::from_request(req) {
147/// Ok(v) => v,
148/// Err(res) => return res, // 400 or 422
149/// };
150/// Response::new()
151/// }
152/// ```
153pub struct Validated<T>(pub T);
154
155impl<T: FromRequest + Validate> FromRequest for Validated<T> {
156 fn from_request(request: &Request) -> Result<Self, Response> {
157 let value = T::from_request(request)?;
158 value.validate().map_err(validation_error_response)?;
159 Ok(Validated(value))
160 }
161}
162
163fn validation_error_response(errors: ValidationErrors) -> Response {
164 let json = errors.into_json();
165 let cr = Range::get_content_range(json.into_bytes(), MimeType::APPLICATION_JSON.to_string());
166 let mut response = Response::new();
167 response.status_code = *STATUS_CODE_REASON_PHRASE.n422_unprocessable_entity.status_code;
168 response.reason_phrase = STATUS_CODE_REASON_PHRASE
169 .n422_unprocessable_entity
170 .reason_phrase
171 .to_string();
172 response.content_range_list = vec![cr];
173 response
174}
175
176/// Returns `true` if `s` is a plausible email address.
177///
178/// Checks for a non-empty local part, exactly one `@`, and a domain that
179/// contains at least one `.` and does not start or end with `.`.
180pub fn is_email(s: &str) -> bool {
181 let mut parts = s.splitn(2, '@');
182 let local = parts.next().unwrap_or("");
183 let domain = match parts.next() {
184 Some(d) => d,
185 None => return false,
186 };
187 !local.is_empty()
188 && domain.len() >= 3
189 && domain.contains('.')
190 && !domain.starts_with('.')
191 && !domain.ends_with('.')
192}
193
194/// Returns `true` if `s` starts with `http://` or `https://`.
195pub fn is_url(s: &str) -> bool {
196 s.starts_with("http://") || s.starts_with("https://")
197}