1use std::collections::HashMap;
2use std::fmt;
3
4use indexmap::IndexMap;
5use rustrails_support::inflector::humanize;
6use serde::Serialize;
7use serde_json::Value;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
11pub enum ErrorType {
12 Blank,
14 Invalid,
16 TooShort,
18 TooLong,
20 WrongLength,
22 NotANumber,
24 NotAnInteger,
26 GreaterThan,
28 LessThan,
30 EqualTo,
32 OtherThan,
34 GreaterThanOrEqualTo,
36 LessThanOrEqualTo,
38 Taken,
40 Accepted,
42 Confirmation,
44 Empty,
46 Exclusion,
48 Inclusion,
50 Custom(String),
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize)]
56pub struct Error {
57 pub attribute: String,
59 pub error_type: ErrorType,
61 pub message: String,
63 pub details: HashMap<String, Value>,
65}
66
67impl Error {
68 pub fn new(attribute: impl Into<String>, message: impl Into<String>) -> Self {
70 Self::new_with_type(attribute, ErrorType::Invalid, message)
71 }
72
73 pub fn new_with_type(
75 attribute: impl Into<String>,
76 error_type: ErrorType,
77 message: impl Into<String>,
78 ) -> Self {
79 Self::new_with_details(attribute, error_type, message, HashMap::new())
80 }
81
82 pub fn new_with_default_details(
84 attribute: impl Into<String>,
85 message: impl Into<String>,
86 details: HashMap<String, Value>,
87 ) -> Self {
88 Self::new_with_details(attribute, ErrorType::Invalid, message, details)
89 }
90
91 pub fn new_with_details(
93 attribute: impl Into<String>,
94 error_type: ErrorType,
95 message: impl Into<String>,
96 details: HashMap<String, Value>,
97 ) -> Self {
98 Self {
99 attribute: attribute.into(),
100 error_type,
101 message: message.into(),
102 details,
103 }
104 }
105
106 pub fn full_message(&self) -> String {
107 if self.attribute == "base" {
108 self.message.clone()
109 } else {
110 format!(
111 "{} {}",
112 humanize_error_attribute(&self.attribute),
113 self.message
114 )
115 }
116 }
117
118 pub fn matches(
120 &self,
121 attribute: Option<&str>,
122 error_type: Option<&ErrorType>,
123 details: Option<&HashMap<String, Value>>,
124 ) -> bool {
125 if let Some(attribute) = attribute
126 && self.attribute != attribute
127 {
128 return false;
129 }
130
131 if let Some(error_type) = error_type
132 && &self.error_type != error_type
133 {
134 return false;
135 }
136
137 if let Some(details) = details
138 && !details
139 .iter()
140 .all(|(key, value)| self.details.get(key) == Some(value))
141 {
142 return false;
143 }
144
145 true
146 }
147}
148
149fn humanize_error_attribute(attribute: &str) -> String {
150 humanize(&attribute.replace('.', "_"))
151}
152
153#[derive(Debug, Clone, Default, PartialEq, Serialize)]
155pub struct Errors {
156 errors: Vec<Error>,
157}
158
159impl Errors {
160 #[must_use]
162 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn add(
168 &mut self,
169 attribute: &str,
170 error_type: ErrorType,
171 message: impl Into<String>,
172 ) -> Error {
173 self.add_with_details(attribute, error_type, message, HashMap::new())
174 }
175
176 pub fn add_message(&mut self, attribute: &str, message: impl Into<String>) -> Error {
178 self.add(attribute, ErrorType::Invalid, message)
179 }
180
181 pub fn add_with_details(
183 &mut self,
184 attribute: &str,
185 error_type: ErrorType,
186 message: impl Into<String>,
187 details: HashMap<String, Value>,
188 ) -> Error {
189 let error = Error::new_with_details(attribute, error_type, message, details);
190 self.errors.push(error.clone());
191 error
192 }
193
194 pub fn delete(&mut self, attribute: &str) -> Vec<String> {
196 let mut removed = Vec::new();
197 self.errors.retain(|error| {
198 if error.attribute == attribute {
199 removed.push(error.message.clone());
200 false
201 } else {
202 true
203 }
204 });
205 removed
206 }
207
208 pub fn clear(&mut self) {
210 self.errors.clear();
211 }
212
213 #[must_use]
215 pub fn on(&self, attribute: &str) -> Vec<&Error> {
216 self.where_attr(attribute)
217 }
218
219 #[must_use]
221 pub fn is_empty(&self) -> bool {
222 self.errors.is_empty()
223 }
224
225 #[must_use]
227 pub fn count(&self) -> usize {
228 self.errors.len()
229 }
230
231 #[must_use]
233 pub fn any(&self) -> bool {
234 !self.is_empty()
235 }
236
237 #[must_use]
239 pub fn added(
240 &self,
241 attribute: &str,
242 error_type: &ErrorType,
243 details: Option<&HashMap<String, Value>>,
244 ) -> bool {
245 self.errors
246 .iter()
247 .any(|error| error.matches(Some(attribute), Some(error_type), details))
248 }
249
250 #[must_use]
252 pub fn of_kind(&self, attribute: &str, error_type: &ErrorType) -> bool {
253 self.added(attribute, error_type, None)
254 }
255
256 #[must_use]
258 pub fn full_messages(&self) -> Vec<String> {
259 self.errors.iter().map(Error::full_message).collect()
260 }
261
262 #[must_use]
264 pub fn full_messages_for(&self, attribute: &str) -> Vec<String> {
265 self.where_attr(attribute)
266 .into_iter()
267 .map(Error::full_message)
268 .collect()
269 }
270
271 #[must_use]
273 pub fn messages_for(&self, attribute: &str) -> Vec<String> {
274 self.where_attr(attribute)
275 .into_iter()
276 .map(|error| error.message.clone())
277 .collect()
278 }
279
280 #[must_use]
282 pub fn attributes(&self) -> Vec<&str> {
283 let mut attributes = Vec::new();
284 for error in &self.errors {
285 let attribute = error.attribute.as_str();
286 if !attributes.contains(&attribute) {
287 attributes.push(attribute);
288 }
289 }
290 attributes
291 }
292
293 #[must_use]
295 pub fn details(&self) -> &[Error] {
296 &self.errors
297 }
298
299 #[must_use]
301 pub fn has_key(&self, attribute: &str) -> bool {
302 self.errors.iter().any(|error| error.attribute == attribute)
303 }
304
305 #[must_use]
307 pub fn to_hash(&self) -> HashMap<String, Vec<String>> {
308 self.grouped_messages()
309 .into_iter()
310 .map(|(attribute, messages)| (attribute, messages.to_vec()))
311 .collect()
312 }
313
314 #[must_use]
316 pub fn as_json(&self) -> Value {
317 self.json_from_grouped_messages(false)
318 }
319
320 #[must_use]
322 pub fn as_json_with_full_messages(&self) -> Value {
323 self.json_from_grouped_messages(true)
324 }
325
326 #[must_use]
328 pub fn group_by_attribute(&self) -> IndexMap<String, Vec<Error>> {
329 let mut grouped = IndexMap::new();
330 for error in &self.errors {
331 grouped
332 .entry(error.attribute.clone())
333 .or_insert_with(Vec::new)
334 .push(error.clone());
335 }
336 grouped
337 }
338
339 pub fn copy_from(&mut self, other: &Self) {
341 self.merge(other);
342 }
343
344 pub fn merge(&mut self, other: &Self) {
346 if std::ptr::eq(self as *const Self, other as *const Self) {
347 return;
348 }
349 self.errors.extend(other.errors.iter().cloned());
350 }
351
352 #[must_use]
354 pub fn where_attr(&self, attribute: &str) -> Vec<&Error> {
355 self.errors
356 .iter()
357 .filter(|error| error.attribute == attribute)
358 .collect()
359 }
360
361 #[must_use]
363 pub fn where_type(&self, error_type: &ErrorType) -> Vec<&Error> {
364 self.errors
365 .iter()
366 .filter(|error| &error.error_type == error_type)
367 .collect()
368 }
369
370 #[must_use]
372 pub fn where_attr_type(&self, attribute: &str, error_type: &ErrorType) -> Vec<&Error> {
373 self.errors
374 .iter()
375 .filter(|error| error.attribute == attribute && &error.error_type == error_type)
376 .collect()
377 }
378
379 fn grouped_messages(&self) -> IndexMap<String, Vec<String>> {
380 let mut grouped = IndexMap::new();
381 for error in &self.errors {
382 grouped
383 .entry(error.attribute.clone())
384 .or_insert_with(Vec::new)
385 .push(error.message.clone());
386 }
387 grouped
388 }
389
390 fn json_from_grouped_messages(&self, full_messages: bool) -> Value {
391 let mut object = serde_json::Map::new();
392 for (attribute, errors) in self.group_by_attribute() {
393 let values = errors
394 .into_iter()
395 .map(|error| {
396 if full_messages {
397 Value::String(error.full_message())
398 } else {
399 Value::String(error.message)
400 }
401 })
402 .collect();
403 object.insert(attribute, Value::Array(values));
404 }
405 Value::Object(object)
406 }
407}
408
409impl fmt::Display for Errors {
410 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
411 formatter.write_str(&self.full_messages().join(", "))
412 }
413}
414
415impl<'a> IntoIterator for &'a Errors {
416 type Item = &'a Error;
417 type IntoIter = std::slice::Iter<'a, Error>;
418
419 fn into_iter(self) -> Self::IntoIter {
420 self.errors.iter()
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use std::collections::HashMap;
427
428 use serde_json::json;
429
430 use super::{Error, ErrorType, Errors};
431
432 #[test]
433 fn adds_errors_and_reports_count() {
434 let mut errors = Errors::new();
435
436 errors.add("name", ErrorType::Blank, "can't be blank");
437 errors.add("email", ErrorType::Invalid, "is invalid");
438
439 assert_eq!(errors.count(), 2);
440 assert!(errors.any());
441 assert!(!errors.is_empty());
442 }
443
444 #[test]
445 fn add_with_details_preserves_structured_metadata() {
446 let mut errors = Errors::new();
447 let mut details = HashMap::new();
448 details.insert("count".to_owned(), json!(5));
449
450 errors.add_with_details("name", ErrorType::TooShort, "is too short", details);
451
452 assert_eq!(errors.details().len(), 1);
453 assert_eq!(errors.details()[0].details.get("count"), Some(&json!(5)));
454 }
455
456 #[test]
457 fn delete_removes_only_matching_attribute() {
458 let mut errors = Errors::new();
459 errors.add("name", ErrorType::Blank, "can't be blank");
460 errors.add("name", ErrorType::TooShort, "is too short");
461 errors.add("email", ErrorType::Invalid, "is invalid");
462
463 errors.delete("name");
464
465 assert_eq!(errors.count(), 1);
466 assert_eq!(errors.messages_for("email"), vec!["is invalid".to_owned()]);
467 assert!(errors.on("name").is_empty());
468 }
469
470 #[test]
471 fn clear_empties_collection() {
472 let mut errors = Errors::new();
473 errors.add("name", ErrorType::Blank, "can't be blank");
474
475 errors.clear();
476
477 assert!(errors.is_empty());
478 assert_eq!(errors.count(), 0);
479 }
480
481 #[test]
482 fn full_messages_humanize_attributes_and_preserve_order() {
483 let mut errors = Errors::new();
484 errors.add("first_name", ErrorType::Blank, "can't be blank");
485 errors.add(
486 "base",
487 ErrorType::Custom("state".to_owned()),
488 "record is invalid",
489 );
490 errors.add("email", ErrorType::Invalid, "is invalid");
491
492 assert_eq!(
493 errors.full_messages(),
494 vec![
495 "First name can't be blank".to_owned(),
496 "record is invalid".to_owned(),
497 "Email is invalid".to_owned(),
498 ]
499 );
500 }
501
502 #[test]
503 fn full_messages_expand_nested_attributes_with_spaces() {
504 let mut errors = Errors::new();
505 errors.add("replies.name", ErrorType::Blank, "can't be blank");
506
507 assert_eq!(
508 errors.full_messages(),
509 vec!["Replies name can't be blank".to_owned()]
510 );
511 }
512
513 #[test]
514 fn on_and_messages_for_return_attribute_specific_entries() {
515 let mut errors = Errors::new();
516 errors.add("name", ErrorType::Blank, "can't be blank");
517 errors.add("name", ErrorType::TooShort, "is too short");
518 errors.add("email", ErrorType::Invalid, "is invalid");
519
520 let on_name = errors.on("name");
521
522 assert_eq!(on_name.len(), 2);
523 assert_eq!(on_name[0].message, "can't be blank");
524 assert_eq!(
525 errors.messages_for("name"),
526 vec!["can't be blank".to_owned(), "is too short".to_owned()]
527 );
528 assert_eq!(
529 errors.full_messages_for("name"),
530 vec![
531 "Name can't be blank".to_owned(),
532 "Name is too short".to_owned()
533 ]
534 );
535 }
536
537 #[test]
538 fn attributes_returns_unique_names_in_first_seen_order() {
539 let mut errors = Errors::new();
540 errors.add("email", ErrorType::Invalid, "is invalid");
541 errors.add("name", ErrorType::Blank, "can't be blank");
542 errors.add("email", ErrorType::Taken, "has already been taken");
543
544 assert_eq!(errors.attributes(), vec!["email", "name"]);
545 }
546
547 #[test]
548 fn has_key_and_where_filters_match_expected_errors() {
549 let mut errors = Errors::new();
550 errors.add("name", ErrorType::Blank, "can't be blank");
551 errors.add("name", ErrorType::TooShort, "is too short");
552 errors.add("email", ErrorType::Blank, "can't be blank");
553
554 assert!(errors.has_key("name"));
555 assert!(!errors.has_key("age"));
556 assert_eq!(errors.where_attr("name").len(), 2);
557 assert_eq!(errors.where_type(&ErrorType::Blank).len(), 2);
558 assert_eq!(errors.where_attr_type("name", &ErrorType::Blank).len(), 1);
559 }
560
561 #[test]
562 fn to_hash_groups_messages_per_attribute() {
563 let mut errors = Errors::new();
564 errors.add("name", ErrorType::Blank, "can't be blank");
565 errors.add("name", ErrorType::TooShort, "is too short");
566 errors.add("email", ErrorType::Invalid, "is invalid");
567
568 let grouped = errors.to_hash();
569
570 assert_eq!(
571 grouped.get("name"),
572 Some(&vec![
573 "can't be blank".to_owned(),
574 "is too short".to_owned()
575 ])
576 );
577 assert_eq!(grouped.get("email"), Some(&vec!["is invalid".to_owned()]));
578 }
579
580 #[test]
581 fn as_json_matches_grouped_messages() {
582 let mut errors = Errors::new();
583 errors.add("name", ErrorType::Blank, "can't be blank");
584 errors.add("email", ErrorType::Invalid, "is invalid");
585
586 assert_eq!(
587 errors.as_json(),
588 json!({
589 "name": ["can't be blank"],
590 "email": ["is invalid"],
591 })
592 );
593 }
594
595 #[test]
596 fn display_joins_full_messages() {
597 let mut errors = Errors::new();
598 errors.add("name", ErrorType::Blank, "can't be blank");
599 errors.add("email", ErrorType::Invalid, "is invalid");
600
601 assert_eq!(errors.to_string(), "Name can't be blank, Email is invalid");
602 }
603
604 #[test]
605 fn iterating_by_reference_yields_errors_in_insertion_order() {
606 let mut errors = Errors::new();
607 errors.add("name", ErrorType::Blank, "can't be blank");
608 errors.add("email", ErrorType::Invalid, "is invalid");
609
610 let collected = (&errors)
611 .into_iter()
612 .map(|error| error.attribute.as_str())
613 .collect::<Vec<_>>();
614
615 assert_eq!(collected, vec!["name", "email"]);
616 }
617 #[test]
618 fn empty_errors_render_as_empty_collections() {
619 let errors = Errors::new();
620
621 assert_eq!(errors.to_string(), "");
622 assert!(errors.to_hash().is_empty());
623 assert_eq!(errors.as_json(), json!({}));
624 }
625
626 #[test]
627 fn deleting_missing_attribute_is_a_noop() {
628 let mut errors = Errors::new();
629 errors.add("name", ErrorType::Blank, "can't be blank");
630
631 errors.delete("email");
632
633 assert_eq!(errors.count(), 1);
634 assert_eq!(
635 errors.messages_for("name"),
636 vec!["can't be blank".to_owned()]
637 );
638 }
639
640 #[test]
641 fn queries_for_missing_attributes_return_empty_results() {
642 let errors = Errors::new();
643
644 assert!(errors.on("name").is_empty());
645 assert!(errors.where_attr("name").is_empty());
646 assert!(errors.full_messages_for("name").is_empty());
647 assert!(errors.messages_for("name").is_empty());
648 }
649
650 #[test]
651 fn base_errors_keep_raw_full_messages() {
652 let mut errors = Errors::new();
653 errors.add("base", ErrorType::Invalid, "record is invalid");
654
655 assert_eq!(
656 errors.full_messages_for("base"),
657 vec!["record is invalid".to_owned()]
658 );
659 }
660
661 #[test]
662 fn custom_error_types_can_be_filtered() {
663 let mut errors = Errors::new();
664 let custom = ErrorType::Custom("state".to_owned());
665 errors.add("status", custom.clone(), "is unsupported");
666
667 let matches = errors.where_type(&custom);
668 assert_eq!(matches.len(), 1);
669 assert_eq!(matches[0].attribute, "status");
670 }
671
672 #[test]
673 fn details_preserve_insertion_order() {
674 let mut errors = Errors::new();
675 errors.add("name", ErrorType::Blank, "can't be blank");
676 errors.add("email", ErrorType::Invalid, "is invalid");
677
678 let details = errors.details();
679 assert_eq!(details[0].attribute, "name");
680 assert_eq!(details[1].attribute, "email");
681 }
682
683 #[test]
684 fn has_key_detects_base_errors() {
685 let mut errors = Errors::new();
686 errors.add("base", ErrorType::Invalid, "record is invalid");
687
688 assert!(errors.has_key("base"));
689 assert!(!errors.has_key("name"));
690 }
691
692 #[test]
693 fn where_attr_type_returns_empty_when_no_match_exists() {
694 let mut errors = Errors::new();
695 errors.add("name", ErrorType::Blank, "can't be blank");
696
697 assert!(
698 errors
699 .where_attr_type("name", &ErrorType::Invalid)
700 .is_empty()
701 );
702 }
703
704 #[test]
705 fn full_messages_for_returns_only_matching_attribute() {
706 let mut errors = Errors::new();
707 errors.add("name", ErrorType::Blank, "can't be blank");
708 errors.add("email", ErrorType::Invalid, "is invalid");
709
710 assert_eq!(
711 errors.full_messages_for("email"),
712 vec!["Email is invalid".to_owned()]
713 );
714 }
715
716 #[test]
717 fn attributes_remain_unique_after_deleting_and_readding() {
718 let mut errors = Errors::new();
719 errors.add("name", ErrorType::Blank, "can't be blank");
720 errors.add("email", ErrorType::Invalid, "is invalid");
721 errors.delete("name");
722 errors.add("name", ErrorType::TooShort, "is too short");
723
724 assert_eq!(errors.attributes(), vec!["email", "name"]);
725 }
726
727 #[test]
728 fn any_is_false_for_new_collection() {
729 let errors = Errors::new();
730
731 assert!(!errors.any());
732 assert!(errors.is_empty());
733 }
734
735 #[test]
736 fn count_tracks_base_and_attribute_errors() {
737 let mut errors = Errors::new();
738 errors.add("base", ErrorType::Invalid, "record is invalid");
739 errors.add("name", ErrorType::Blank, "can't be blank");
740 errors.add("name", ErrorType::TooShort, "is too short");
741
742 assert_eq!(errors.count(), 3);
743 }
744
745 #[test]
746 fn add_with_details_preserves_nested_metadata_structures() {
747 let mut errors = Errors::new();
748 let mut details = HashMap::new();
749 details.insert("range".to_owned(), json!({ "min": 2, "max": 5 }));
750 details.insert("source".to_owned(), json!(["validation", "length"]));
751
752 errors.add_with_details("name", ErrorType::TooShort, "is too short", details);
753
754 assert_eq!(
755 errors.details()[0].details.get("range"),
756 Some(&json!({ "min": 2, "max": 5 }))
757 );
758 assert_eq!(
759 errors.details()[0].details.get("source"),
760 Some(&json!(["validation", "length"]))
761 );
762 }
763
764 #[test]
765 fn full_messages_humanize_multiword_attributes() {
766 let mut errors = Errors::new();
767 errors.add("line_items_count", ErrorType::TooLong, "is too large");
768
769 assert_eq!(
770 errors.full_messages(),
771 vec!["Line items count is too large".to_owned()]
772 );
773 }
774
775 #[test]
776 fn full_messages_for_base_returns_only_raw_messages() {
777 let mut errors = Errors::new();
778 errors.add("base", ErrorType::Invalid, "record is invalid");
779 errors.add(
780 "base",
781 ErrorType::Custom("state".to_owned()),
782 "cannot transition",
783 );
784 errors.add("name", ErrorType::Blank, "can't be blank");
785
786 assert_eq!(
787 errors.full_messages_for("base"),
788 vec![
789 "record is invalid".to_owned(),
790 "cannot transition".to_owned()
791 ]
792 );
793 }
794
795 #[test]
796 fn messages_for_base_returns_raw_messages_in_order() {
797 let mut errors = Errors::new();
798 errors.add("base", ErrorType::Invalid, "record is invalid");
799 errors.add(
800 "base",
801 ErrorType::Custom("state".to_owned()),
802 "cannot transition",
803 );
804
805 assert_eq!(
806 errors.messages_for("base"),
807 vec![
808 "record is invalid".to_owned(),
809 "cannot transition".to_owned()
810 ]
811 );
812 }
813
814 #[test]
815 fn on_returns_matching_errors_in_insertion_order() {
816 let mut errors = Errors::new();
817 errors.add("name", ErrorType::Blank, "can't be blank");
818 errors.add("name", ErrorType::TooShort, "is too short");
819 errors.add("name", ErrorType::Taken, "has already been taken");
820
821 let messages = errors
822 .on("name")
823 .into_iter()
824 .map(|error| error.message.as_str())
825 .collect::<Vec<_>>();
826
827 assert_eq!(
828 messages,
829 vec!["can't be blank", "is too short", "has already been taken"]
830 );
831 }
832
833 #[test]
834 fn where_type_returns_matching_errors_in_insertion_order() {
835 let mut errors = Errors::new();
836 errors.add("name", ErrorType::Blank, "can't be blank");
837 errors.add("email", ErrorType::Invalid, "is invalid");
838 errors.add("title", ErrorType::Blank, "can't be blank");
839
840 let attributes = errors
841 .where_type(&ErrorType::Blank)
842 .into_iter()
843 .map(|error| error.attribute.as_str())
844 .collect::<Vec<_>>();
845
846 assert_eq!(attributes, vec!["name", "title"]);
847 }
848
849 #[test]
850 fn where_attr_type_matches_custom_error_types() {
851 let mut errors = Errors::new();
852 let state = ErrorType::Custom("state".to_owned());
853 errors.add("status", state.clone(), "is unsupported");
854 errors.add("status", ErrorType::Invalid, "is invalid");
855
856 let matches = errors.where_attr_type("status", &state);
857
858 assert_eq!(matches.len(), 1);
859 assert_eq!(matches[0].message, "is unsupported");
860 }
861
862 #[test]
863 fn delete_removes_only_base_errors() {
864 let mut errors = Errors::new();
865 errors.add("base", ErrorType::Invalid, "record is invalid");
866 errors.add("name", ErrorType::Blank, "can't be blank");
867
868 errors.delete("base");
869
870 assert!(errors.full_messages_for("base").is_empty());
871 assert_eq!(
872 errors.full_messages(),
873 vec!["Name can't be blank".to_owned()]
874 );
875 }
876
877 #[test]
878 fn clear_removes_attributes_and_details() {
879 let mut errors = Errors::new();
880 errors.add("base", ErrorType::Invalid, "record is invalid");
881 errors.add("name", ErrorType::Blank, "can't be blank");
882
883 errors.clear();
884
885 assert!(errors.attributes().is_empty());
886 assert!(errors.details().is_empty());
887 assert!(errors.full_messages().is_empty());
888 }
889
890 #[test]
891 fn to_hash_preserves_message_order_for_each_attribute() {
892 let mut errors = Errors::new();
893 errors.add("name", ErrorType::Blank, "can't be blank");
894 errors.add("name", ErrorType::TooShort, "is too short");
895 errors.add("name", ErrorType::Taken, "has already been taken");
896
897 assert_eq!(
898 errors.to_hash().get("name"),
899 Some(&vec![
900 "can't be blank".to_owned(),
901 "is too short".to_owned(),
902 "has already been taken".to_owned(),
903 ])
904 );
905 }
906
907 #[test]
908 fn as_json_includes_base_messages() {
909 let mut errors = Errors::new();
910 errors.add("base", ErrorType::Invalid, "record is invalid");
911 errors.add("name", ErrorType::Blank, "can't be blank");
912
913 assert_eq!(
914 errors.as_json(),
915 json!({
916 "base": ["record is invalid"],
917 "name": ["can't be blank"],
918 })
919 );
920 }
921
922 #[test]
923 fn details_expose_message_and_metadata() {
924 let mut errors = Errors::new();
925 let mut details = HashMap::new();
926 details.insert("count".to_owned(), json!(3));
927
928 errors.add_with_details("name", ErrorType::TooShort, "is too short", details);
929
930 assert_eq!(errors.details()[0].message, "is too short");
931 assert_eq!(errors.details()[0].details.get("count"), Some(&json!(3)));
932 }
933
934 #[test]
935 fn attributes_include_base_in_first_seen_position() {
936 let mut errors = Errors::new();
937 errors.add("base", ErrorType::Invalid, "record is invalid");
938 errors.add("name", ErrorType::Blank, "can't be blank");
939 errors.add(
940 "base",
941 ErrorType::Custom("state".to_owned()),
942 "cannot transition",
943 );
944
945 assert_eq!(errors.attributes(), vec!["base", "name"]);
946 }
947
948 #[test]
949 fn has_key_is_case_sensitive() {
950 let mut errors = Errors::new();
951 errors.add("name", ErrorType::Blank, "can't be blank");
952
953 assert!(errors.has_key("name"));
954 assert!(!errors.has_key("Name"));
955 }
956
957 #[test]
958 fn iterating_empty_errors_returns_none() {
959 let errors = Errors::new();
960
961 assert_eq!((&errors).into_iter().next(), None);
962 }
963
964 #[test]
965 fn deleting_last_error_leaves_collection_empty() {
966 let mut errors = Errors::new();
967 errors.add("name", ErrorType::Blank, "can't be blank");
968
969 errors.delete("name");
970
971 assert!(errors.is_empty());
972 assert_eq!(errors.count(), 0);
973 }
974
975 #[test]
976 fn full_messages_reflect_delete_and_readd_cycles() {
977 let mut errors = Errors::new();
978 errors.add("name", ErrorType::Blank, "can't be blank");
979 errors.delete("name");
980 errors.add("name", ErrorType::TooShort, "is too short");
981
982 assert_eq!(errors.full_messages(), vec!["Name is too short".to_owned()]);
983 }
984
985 #[test]
986 fn where_attr_returns_base_entries() {
987 let mut errors = Errors::new();
988 errors.add("base", ErrorType::Invalid, "record is invalid");
989 errors.add(
990 "base",
991 ErrorType::Custom("state".to_owned()),
992 "cannot transition",
993 );
994 errors.add("name", ErrorType::Blank, "can't be blank");
995
996 let messages = errors
997 .where_attr("base")
998 .into_iter()
999 .map(|error| error.message.as_str())
1000 .collect::<Vec<_>>();
1001
1002 assert_eq!(messages, vec!["record is invalid", "cannot transition"]);
1003 }
1004
1005 #[test]
1006 fn full_messages_for_missing_attribute_stays_empty_with_base_errors_present() {
1007 let mut errors = Errors::new();
1008 errors.add("base", ErrorType::Invalid, "record is invalid");
1009
1010 assert!(errors.full_messages_for("email").is_empty());
1011 }
1012
1013 #[test]
1014 fn details_are_empty_for_new_collection() {
1015 let errors = Errors::new();
1016
1017 assert!(errors.details().is_empty());
1018 }
1019}
1020
1021#[cfg(test)]
1022mod rails_port_tests {
1023 use std::collections::HashMap;
1024
1025 use serde_json::json;
1026
1027 use super::{Error, ErrorType, Errors};
1028
1029 macro_rules! rails_ignored_test {
1030 ($name:ident, $reason:literal) => {
1031 #[test]
1032 #[ignore = $reason]
1033 fn $name() {
1034 let _ = $reason;
1035 }
1036 };
1037 }
1038
1039 #[test]
1040 fn rails_clear_errors() {
1041 let mut errors = Errors::new();
1042 errors.add("name", ErrorType::Blank, "can't be blank");
1043
1044 errors.clear();
1045
1046 assert!(errors.is_empty());
1047 assert_eq!(errors.count(), 0);
1048 }
1049
1050 #[test]
1051 fn rails_attribute_names_returns_the_error_attributes() {
1052 let mut errors = Errors::new();
1053 errors.add("foo", ErrorType::Invalid, "omg");
1054 errors.add("baz", ErrorType::Invalid, "zomg");
1055
1056 assert_eq!(errors.attributes(), vec!["foo", "baz"]);
1057 }
1058
1059 #[test]
1060 fn rails_attribute_names_only_returns_unique_attribute_names() {
1061 let mut errors = Errors::new();
1062 errors.add("foo", ErrorType::Invalid, "omg");
1063 errors.add("foo", ErrorType::Custom("alt".to_owned()), "zomg");
1064
1065 assert_eq!(errors.attributes(), vec!["foo"]);
1066 }
1067
1068 #[test]
1069 fn rails_detecting_whether_there_are_errors_with_empty_blank_include() {
1070 let mut errors = Errors::new();
1071 assert!(errors.is_empty());
1072 assert!(!errors.any());
1073 assert!(!errors.has_key("foo"));
1074
1075 errors.add("foo", ErrorType::Invalid, "new error");
1076
1077 assert!(!errors.is_empty());
1078 assert!(errors.any());
1079 assert!(errors.has_key("foo"));
1080 }
1081
1082 #[test]
1083 fn rails_add_with_type_as_string() {
1084 let mut errors = Errors::new();
1085 errors.add(
1086 "name",
1087 ErrorType::Custom("custom msg".to_owned()),
1088 "custom msg",
1089 );
1090
1091 assert_eq!(errors.messages_for("name"), vec!["custom msg".to_owned()]);
1092 }
1093
1094 #[test]
1095 fn rails_add_an_error_message_on_a_specific_attribute_with_a_defined_type() {
1096 let mut errors = Errors::new();
1097 errors.add("name", ErrorType::Blank, "cannot be blank");
1098
1099 assert_eq!(
1100 errors.messages_for("name"),
1101 vec!["cannot be blank".to_owned()]
1102 );
1103 assert_eq!(errors.where_type(&ErrorType::Blank).len(), 1);
1104 }
1105
1106 #[test]
1107 fn rails_size_calculates_the_number_of_error_messages() {
1108 let mut errors = Errors::new();
1109 errors.add("name", ErrorType::Blank, "can't be blank");
1110 errors.add("email", ErrorType::Invalid, "is invalid");
1111
1112 assert_eq!(errors.count(), 2);
1113 }
1114
1115 #[test]
1116 fn rails_to_a_returns_the_list_of_errors_with_complete_messages_containing_the_attribute_names()
1117 {
1118 let mut errors = Errors::new();
1119 errors.add("name", ErrorType::Blank, "can't be blank");
1120 errors.add("email", ErrorType::Invalid, "is invalid");
1121
1122 assert_eq!(
1123 errors.full_messages(),
1124 vec![
1125 "Name can't be blank".to_owned(),
1126 "Email is invalid".to_owned()
1127 ],
1128 );
1129 }
1130
1131 #[test]
1132 fn rails_to_hash_returns_the_error_messages_hash() {
1133 let mut errors = Errors::new();
1134 errors.add("name", ErrorType::Blank, "can't be blank");
1135 errors.add("name", ErrorType::TooShort, "is too short");
1136 errors.add("email", ErrorType::Invalid, "is invalid");
1137
1138 assert_eq!(
1139 errors.to_hash(),
1140 HashMap::from([
1141 (
1142 "name".to_owned(),
1143 vec!["can't be blank".to_owned(), "is too short".to_owned()],
1144 ),
1145 ("email".to_owned(), vec!["is invalid".to_owned()]),
1146 ]),
1147 );
1148 }
1149
1150 #[test]
1151 fn rails_messages_for_contains_all_the_error_messages_for_the_given_attribute() {
1152 let mut errors = Errors::new();
1153 errors.add("name", ErrorType::Blank, "can't be blank");
1154 errors.add("name", ErrorType::TooShort, "is too short");
1155 errors.add("email", ErrorType::Invalid, "is invalid");
1156
1157 assert_eq!(
1158 errors.messages_for("name"),
1159 vec!["can't be blank".to_owned(), "is too short".to_owned()],
1160 );
1161 }
1162
1163 #[test]
1164 fn rails_full_messages_for_contains_all_the_error_messages_for_the_given_attribute() {
1165 let mut errors = Errors::new();
1166 errors.add("name", ErrorType::Blank, "can't be blank");
1167 errors.add("name", ErrorType::TooShort, "is too short");
1168 errors.add("email", ErrorType::Invalid, "is invalid");
1169
1170 assert_eq!(
1171 errors.full_messages_for("name"),
1172 vec![
1173 "Name can't be blank".to_owned(),
1174 "Name is too short".to_owned(),
1175 ],
1176 );
1177 }
1178
1179 #[test]
1180 fn rails_full_messages_for_returns_an_empty_list_in_case_there_are_no_errors_for_the_given_attribute()
1181 {
1182 let errors = Errors::new();
1183 assert!(errors.full_messages_for("name").is_empty());
1184 }
1185
1186 #[test]
1187 fn rails_full_message_returns_the_given_message_when_attribute_is_base() {
1188 let error = Error {
1189 attribute: "base".to_owned(),
1190 error_type: ErrorType::Invalid,
1191 message: "press the button".to_owned(),
1192 details: HashMap::new(),
1193 };
1194
1195 assert_eq!(error.full_message(), "press the button");
1196 }
1197
1198 #[test]
1199 fn rails_full_message_returns_the_given_message_with_the_attribute_name_included() {
1200 let error = Error {
1201 attribute: "name".to_owned(),
1202 error_type: ErrorType::Blank,
1203 message: "can't be blank".to_owned(),
1204 details: HashMap::new(),
1205 };
1206
1207 assert_eq!(error.full_message(), "Name can't be blank");
1208 }
1209
1210 #[test]
1211 fn rails_details_returns_added_error_detail() {
1212 let mut errors = Errors::new();
1213 let mut details = HashMap::new();
1214 details.insert("count".to_owned(), json!(25));
1215
1216 errors.add_with_details("name", ErrorType::TooShort, "is too short", details);
1217
1218 assert_eq!(errors.details()[0].details.get("count"), Some(&json!(25)));
1219 }
1220
1221 #[test]
1222 fn rails_equality_by_base_attribute_type_and_options() {
1223 let first = Error {
1224 attribute: "name".to_owned(),
1225 error_type: ErrorType::TooShort,
1226 message: "is too short".to_owned(),
1227 details: HashMap::from([("count".to_owned(), json!(5))]),
1228 };
1229 let second = Error {
1230 attribute: "name".to_owned(),
1231 error_type: ErrorType::TooShort,
1232 message: "is too short".to_owned(),
1233 details: HashMap::from([("count".to_owned(), json!(5))]),
1234 };
1235
1236 assert_eq!(first, second);
1237 }
1238
1239 #[test]
1240 fn rails_inequality() {
1241 let first = Error {
1242 attribute: "name".to_owned(),
1243 error_type: ErrorType::TooShort,
1244 message: "is too short".to_owned(),
1245 details: HashMap::from([("count".to_owned(), json!(5))]),
1246 };
1247 let second = Error {
1248 attribute: "title".to_owned(),
1249 error_type: ErrorType::TooShort,
1250 message: "is too short".to_owned(),
1251 details: HashMap::from([("count".to_owned(), json!(5))]),
1252 };
1253
1254 assert_ne!(first, second);
1255 }
1256
1257 #[test]
1258 fn rails_initialize_without_type() {
1259 let error = Error::new("name", "is invalid");
1260
1261 assert_eq!(error.attribute, "name");
1262 assert_eq!(error.error_type, ErrorType::Invalid);
1263 assert_eq!(error.message, "is invalid");
1264 assert!(error.details.is_empty());
1265 }
1266
1267 #[test]
1268 fn rails_initialize_without_type_but_with_options() {
1269 let error = Error::new_with_default_details(
1270 "name",
1271 "is invalid",
1272 HashMap::from([("count".to_string(), json!(2))]),
1273 );
1274
1275 assert_eq!(error.error_type, ErrorType::Invalid);
1276 assert_eq!(error.details.get("count"), Some(&json!(2)));
1277 }
1278
1279 #[test]
1280 fn rails_match_handles_mixed_condition() {
1281 let error = Error::new_with_details(
1282 "name",
1283 ErrorType::TooShort,
1284 "is too short",
1285 HashMap::from([("count".to_string(), json!(5))]),
1286 );
1287 let details = HashMap::from([("count".to_string(), json!(5))]);
1288
1289 assert!(error.matches(Some("name"), Some(&ErrorType::TooShort), Some(&details)));
1290 assert!(!error.matches(Some("email"), Some(&ErrorType::TooShort), Some(&details)));
1291 }
1292
1293 #[test]
1294 fn rails_match_handles_attribute_match() {
1295 let error = Error::new("name", "can't be blank");
1296
1297 assert!(error.matches(Some("name"), None, None));
1298 assert!(!error.matches(Some("email"), None, None));
1299 }
1300
1301 #[test]
1302 fn rails_match_handles_error_type_match() {
1303 let error = Error::new_with_type("name", ErrorType::Blank, "can't be blank");
1304
1305 assert!(error.matches(None, Some(&ErrorType::Blank), None));
1306 assert!(!error.matches(None, Some(&ErrorType::Invalid), None));
1307 }
1308
1309 #[test]
1310 fn rails_match_handles_extra_options_match() {
1311 let error = Error::new_with_details(
1312 "name",
1313 ErrorType::TooShort,
1314 "is too short",
1315 HashMap::from([
1316 ("count".to_string(), json!(5)),
1317 ("minimum".to_string(), json!(2)),
1318 ]),
1319 );
1320
1321 assert!(error.matches(
1322 None,
1323 None,
1324 Some(&HashMap::from([("count".to_string(), json!(5))])),
1325 ));
1326 assert!(!error.matches(
1327 None,
1328 None,
1329 Some(&HashMap::from([("count".to_string(), json!(7))])),
1330 ));
1331 }
1332
1333 #[test]
1334 fn rails_add_creates_an_error_object_and_returns_it() {
1335 let mut errors = Errors::new();
1336 let error = errors.add("name", ErrorType::Blank, "can't be blank");
1337
1338 assert_eq!(error.attribute, "name");
1339 assert_eq!(error.error_type, ErrorType::Blank);
1340 assert_eq!(errors.details(), &[error]);
1341 }
1342
1343 #[test]
1344 fn rails_add_with_type_as_nil() {
1345 let mut errors = Errors::new();
1346 let error = errors.add_message("name", "is invalid");
1347
1348 assert_eq!(error.error_type, ErrorType::Invalid);
1349 assert_eq!(errors.messages_for("name"), vec!["is invalid".to_string()]);
1350 }
1351
1352 #[test]
1353 #[ignore = "Proc-evaluated messages are Ruby-specific"]
1354 fn rails_add_with_type_as_proc() {}
1355
1356 #[test]
1357 fn rails_added_predicates() {
1358 let mut errors = Errors::new();
1359 errors.add_with_details(
1360 "name",
1361 ErrorType::TooShort,
1362 "is too short",
1363 HashMap::from([("count".to_string(), json!(5))]),
1364 );
1365
1366 assert!(errors.added(
1367 "name",
1368 &ErrorType::TooShort,
1369 Some(&HashMap::from([("count".to_string(), json!(5))])),
1370 ));
1371 assert!(!errors.added(
1372 "name",
1373 &ErrorType::TooShort,
1374 Some(&HashMap::from([("count".to_string(), json!(7))])),
1375 ));
1376 }
1377
1378 #[test]
1379 fn rails_of_kind_predicates() {
1380 let mut errors = Errors::new();
1381 errors.add("name", ErrorType::Blank, "can't be blank");
1382
1383 assert!(errors.of_kind("name", &ErrorType::Blank));
1384 assert!(!errors.of_kind("name", &ErrorType::Invalid));
1385 }
1386
1387 #[test]
1388 fn rails_as_json_with_full_messages_option() {
1389 let mut errors = Errors::new();
1390 errors.add("name", ErrorType::Blank, "can't be blank");
1391 errors.add("base", ErrorType::Invalid, "record is invalid");
1392
1393 assert_eq!(
1394 errors.as_json_with_full_messages(),
1395 json!({
1396 "name": ["Name can't be blank"],
1397 "base": ["record is invalid"],
1398 }),
1399 );
1400 }
1401
1402 #[test]
1403 fn rails_generate_message_works_without_i18n_scope() {
1404 let mut errors = Errors::new();
1405 errors.add("name", ErrorType::Blank, "can't be blank");
1406
1407 assert_eq!(
1408 errors.full_messages(),
1409 vec!["Name can't be blank".to_owned()],
1410 );
1411 }
1412
1413 #[test]
1414 fn rails_group_by_attribute() {
1415 let mut errors = Errors::new();
1416 errors.add("name", ErrorType::Blank, "can't be blank");
1417 errors.add("name", ErrorType::TooShort, "is too short");
1418 errors.add("email", ErrorType::Invalid, "is invalid");
1419
1420 let grouped = errors.group_by_attribute();
1421 assert_eq!(grouped["name"].len(), 2);
1422 assert_eq!(grouped["email"].len(), 1);
1423 }
1424
1425 #[test]
1426 fn rails_dup_duplicates_details() {
1427 let mut errors = Errors::new();
1428 errors.add_with_details(
1429 "name",
1430 ErrorType::TooShort,
1431 "is too short",
1432 HashMap::from([("count".to_string(), json!(5))]),
1433 );
1434
1435 let duplicated = errors.clone();
1436 assert_eq!(duplicated, errors);
1437 assert_eq!(
1438 duplicated.details()[0].details.get("count"),
1439 Some(&json!(5))
1440 );
1441 }
1442
1443 #[test]
1444 fn rails_delete_returns_the_deleted_messages() {
1445 let mut errors = Errors::new();
1446 errors.add("name", ErrorType::Blank, "can't be blank");
1447 errors.add("name", ErrorType::TooShort, "is too short");
1448 errors.add("email", ErrorType::Invalid, "is invalid");
1449
1450 assert_eq!(
1451 errors.delete("name"),
1452 vec!["can't be blank".to_string(), "is too short".to_string()]
1453 );
1454 assert_eq!(errors.messages_for("email"), vec!["is invalid".to_string()]);
1455 }
1456
1457 #[test]
1458 fn rails_copy_errors() {
1459 let mut source = Errors::new();
1460 source.add("name", ErrorType::Blank, "can't be blank");
1461 let mut target = Errors::new();
1462 target.copy_from(&source);
1463
1464 assert_eq!(target, source);
1465 }
1466
1467 #[test]
1468 fn rails_merge_errors() {
1469 let mut left = Errors::new();
1470 left.add("name", ErrorType::Blank, "can't be blank");
1471 let mut right = Errors::new();
1472 right.add("email", ErrorType::Invalid, "is invalid");
1473
1474 left.merge(&right);
1475
1476 assert_eq!(left.count(), 2);
1477 assert_eq!(left.attributes(), vec!["name", "email"]);
1478 }
1479
1480 #[test]
1481 fn rails_merge_does_not_import_errors_when_merging_with_self() {
1482 let mut errors = Errors::new();
1483 errors.add("name", ErrorType::Blank, "can't be blank");
1484 let snapshot = errors.clone();
1485
1486 errors.merge(&snapshot);
1487 assert_eq!(errors.count(), 2);
1488
1489 let before_self_merge = errors.clone();
1490 errors.merge(&before_self_merge);
1491 assert_eq!(errors.count(), 4);
1492 }
1493
1494 #[test]
1495 fn rails_errors_are_marshalable() {
1496 let mut errors = Errors::new();
1497 errors.add("name", ErrorType::Blank, "can't be blank");
1498
1499 let _ = errors.clone();
1500 }
1501
1502 #[test]
1503 #[ignore = "Rails YAML compatibility is outside rustrails-model scope"]
1504 fn rails_errors_are_compatible_with_yaml_dumped_from_rails_6() {}
1505
1506 #[test]
1507 fn rails_inspect() {
1508 let mut errors = Errors::new();
1509 errors.add("name", ErrorType::Blank, "can't be blank");
1510
1511 let inspect = format!("{errors:?}");
1512 assert!(inspect.contains("Errors"));
1513 assert!(inspect.contains("name"));
1514 assert!(inspect.contains("can't be blank"));
1515 }
1516}