rustlavel_validation/
errors.rs1use crate::messages::Messages;
9use rustlavel_core::Json;
10use rustlavel_http::{IntoResponse, Response, Status};
11use std::collections::BTreeMap;
12
13#[derive(Debug, Clone, Default, PartialEq)]
19pub struct Errors {
20 fields: BTreeMap<String, Vec<String>>,
21 wants_json: bool,
22 back: Option<String>,
26}
27
28impl Errors {
29 pub fn new() -> Self {
30 Errors::default()
31 }
32
33 pub const STATUS: Status = Status::UNPROCESSABLE;
35
36 pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
37 self.fields.entry(field.into()).or_default().push(message.into());
38 }
39
40 pub fn has(&self, field: &str) -> bool {
41 self.fields.contains_key(field)
42 }
43
44 pub fn first(&self, field: &str) -> Option<&str> {
46 self.fields.get(field)?.first().map(String::as_str)
47 }
48
49 pub fn get(&self, field: &str) -> &[String] {
51 self.fields.get(field).map_or(&[], Vec::as_slice)
52 }
53
54 pub fn all(&self) -> &BTreeMap<String, Vec<String>> {
56 &self.fields
57 }
58
59 pub fn messages(&self) -> impl Iterator<Item = &str> {
61 self.fields.values().flatten().map(String::as_str)
62 }
63
64 pub fn is_empty(&self) -> bool {
65 self.fields.is_empty()
66 }
67
68 pub fn len(&self) -> usize {
71 self.fields.values().map(Vec::len).sum()
72 }
73
74 pub fn fields(&self) -> impl Iterator<Item = &str> {
75 self.fields.keys().map(String::as_str)
76 }
77
78 pub fn wants_json(&self) -> bool {
80 self.wants_json
81 }
82
83 pub fn redirecting_to(mut self, back: Option<String>) -> Self {
85 self.back = back;
86 self
87 }
88
89 pub fn back(&self) -> Option<&str> {
91 self.back.as_deref()
92 }
93
94 pub fn with_json(mut self, wants_json: bool) -> Self {
95 self.wants_json = wants_json;
96 self
97 }
98
99 pub fn with(mut self, field: impl Into<String>, message: impl Into<String>) -> Self {
103 self.add(field, message);
104 self
105 }
106
107 pub fn add_interpolated(&mut self, messages: &Messages, field: &str, template: &str) {
110 let rendered =
111 crate::messages::interpolate(template, &[("attribute", messages.label(field))]);
112 self.add(field, rendered);
113 }
114
115 pub fn summary(&self) -> String {
119 let Some(first) = self.messages().next() else {
120 return "The given data was invalid.".to_string();
121 };
122 match self.len() - 1 {
123 0 => first.to_string(),
124 1 => format!("{first} (and 1 more error)"),
125 more => format!("{first} (and {more} more errors)"),
126 }
127 }
128
129 pub fn to_field_json(&self) -> Json {
136 Json::object(self.fields.iter().map(|(field, messages)| {
137 (
138 field.as_str(),
139 Json::Array(messages.iter().map(|m| Json::from(m.as_str())).collect()),
140 )
141 }))
142 }
143
144 pub fn to_json(&self) -> Json {
145 let errors = self.fields.iter().map(|(field, messages)| {
146 let messages = messages.iter().map(|m| Json::from(m.as_str())).collect();
147 (field.clone(), Json::Array(messages))
148 });
149 Json::object([
150 ("message", Json::from(self.summary())),
151 ("errors", Json::Object(errors.collect())),
152 ])
153 }
154}
155
156impl std::fmt::Display for Errors {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.write_str(&self.summary())
159 }
160}
161
162impl std::error::Error for Errors {}
163
164impl From<&Errors> for Json {
165 fn from(errors: &Errors) -> Self {
166 errors.to_json()
167 }
168}
169
170impl From<Errors> for Json {
171 fn from(errors: Errors) -> Self {
172 errors.to_json()
173 }
174}
175
176impl IntoResponse for Errors {
193 fn into_response(self) -> Response {
194 if self.wants_json {
195 return Response::new(Errors::STATUS).with_json(self.to_json());
196 }
197 if let Some(back) = &self.back {
198 return Response::see_other(back.clone());
201 }
202 let mut body = self.summary();
203 for message in self.messages().skip(1) {
204 body.push('\n');
205 body.push_str(message);
206 }
207 Response::new(Errors::STATUS).with_text(body)
208 }
209}
210
211impl From<Errors> for Response {
212 fn from(errors: Errors) -> Self {
213 errors.into_response()
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 fn sample() -> Errors {
222 Errors::new()
223 .with("email", "The email field is required.")
224 .with("email", "The email field must be a valid email address.")
225 .with("age", "The age field must be at least 18.")
226 }
227
228 #[test]
229 fn an_empty_bag_reports_itself_as_empty() {
230 let errors = Errors::new();
231 assert!(errors.is_empty());
232 assert_eq!(errors.len(), 0);
233 assert!(!errors.has("email"));
234 assert_eq!(errors.first("email"), None);
235 assert!(errors.get("email").is_empty());
236 }
237
238 #[test]
239 fn messages_are_grouped_by_field_and_kept_in_order() {
240 let errors = sample();
241
242 assert!(errors.has("email"));
243 assert_eq!(errors.first("email"), Some("The email field is required."));
244 assert_eq!(errors.get("email").len(), 2);
245 assert_eq!(errors.all().len(), 2, "two fields failed");
246 assert_eq!(errors.len(), 3, "three messages in total");
247 assert_eq!(errors.fields().collect::<Vec<_>>(), ["age", "email"]);
248 }
249
250 #[test]
251 fn the_summary_counts_the_failures_it_did_not_show() {
252 assert_eq!(Errors::new().summary(), "The given data was invalid.");
253 assert_eq!(Errors::new().with("a", "One.").summary(), "One.");
254 assert_eq!(
255 Errors::new().with("a", "One.").with("a", "Two.").summary(),
256 "One. (and 1 more error)"
257 );
258 assert_eq!(sample().summary(), "The age field must be at least 18. (and 2 more errors)");
259 }
260
261 #[test]
262 fn the_body_has_laravels_422_shape() {
263 let body = Errors::new()
264 .with("email", "The email field is required.")
265 .to_json();
266
267 assert_eq!(
268 body.to_string(),
269 r#"{"errors":{"email":["The email field is required."]},"message":"The email field is required."}"#
270 );
271 assert_eq!(body.get("errors.email.0").unwrap().as_str(), Some("The email field is required."));
272 }
273
274 #[test]
275 fn a_json_client_gets_the_422_envelope() {
276 let response = sample().with_json(true).into_response();
277
278 assert_eq!(response.status, Status::UNPROCESSABLE);
279 assert_eq!(response.headers.content_type(), Some("application/json"));
280 assert!(response.body_string().contains(r#""errors":{"age":["#));
281 }
282
283 #[test]
284 fn a_browser_gets_a_plain_body_it_can_read() {
285 let response = sample().into_response();
286
287 assert_eq!(response.status, Status::UNPROCESSABLE);
288 assert_eq!(response.headers.content_type(), Some("text/plain"));
289 assert!(response.body_string().contains("The email field is required."));
290 }
291
292 #[test]
293 fn a_hand_written_message_interpolates_the_attribute() {
294 let messages = Messages::new().attribute("dob", "date of birth");
295 let mut errors = Errors::new();
296 errors.add_interpolated(&messages, "dob", "The :attribute field is in the future.");
297
298 assert_eq!(errors.first("dob"), Some("The date of birth field is in the future."));
299 }
300
301 #[test]
302 fn errors_display_as_their_summary() {
303 assert_eq!(sample().to_string(), sample().summary());
304 }
305}