1use crate::rule::Rule;
11use std::collections::BTreeMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SizeKind {
19 Numeric,
20 String,
21 Array,
22}
23
24#[derive(Debug, Clone, Default, PartialEq)]
26pub struct Messages {
27 overrides: BTreeMap<String, String>,
28 attributes: BTreeMap<String, String>,
29}
30
31impl Messages {
32 pub fn new() -> Self {
33 Messages::default()
34 }
35
36 pub fn set(&mut self, key: impl Into<String>, message: impl Into<String>) {
42 self.overrides.insert(key.into(), message.into());
43 }
44
45 pub fn with(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
47 self.set(key, message);
48 self
49 }
50
51 pub fn set_attribute(&mut self, field: impl Into<String>, label: impl Into<String>) {
53 self.attributes.insert(field.into(), label.into());
54 }
55
56 pub fn attribute(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
58 self.set_attribute(field, label);
59 self
60 }
61
62 pub fn label(&self, field: &str) -> String {
67 match self.attributes.get(field) {
68 Some(label) => label.clone(),
69 None => field.replace(['_', '-', '.'], " "),
70 }
71 }
72
73 pub fn template(&self, field: &str, rule: &Rule, kind: SizeKind) -> String {
76 let name = rule.name();
77 self.overrides
78 .get(&format!("{field}.{name}"))
79 .or_else(|| self.overrides.get(name))
80 .cloned()
81 .unwrap_or_else(|| default_template(rule, kind).to_string())
82 }
83}
84
85pub fn default_template(rule: &Rule, kind: SizeKind) -> &'static str {
87 match rule {
88 Rule::Required => "The :attribute field is required.",
89 Rule::Nullable => "The :attribute field is invalid.",
91 Rule::String => "The :attribute field must be a string.",
92 Rule::Integer => "The :attribute field must be an integer.",
93 Rule::Numeric => "The :attribute field must be a number.",
94 Rule::Boolean => "The :attribute field must be true or false.",
95 Rule::Email => "The :attribute field must be a valid email address.",
96 Rule::Url => "The :attribute field must be a valid URL.",
97 Rule::Min(_) => match kind {
98 SizeKind::Numeric => "The :attribute field must be at least :min.",
99 SizeKind::String => "The :attribute field must be at least :min characters.",
100 SizeKind::Array => "The :attribute field must have at least :min items.",
101 },
102 Rule::Max(_) => match kind {
103 SizeKind::Numeric => "The :attribute field must not be greater than :max.",
104 SizeKind::String => "The :attribute field must not be greater than :max characters.",
105 SizeKind::Array => "The :attribute field must not have more than :max items.",
106 },
107 Rule::Between(_, _) => match kind {
108 SizeKind::Numeric => "The :attribute field must be between :min and :max.",
109 SizeKind::String => "The :attribute field must be between :min and :max characters.",
110 SizeKind::Array => "The :attribute field must have between :min and :max items.",
111 },
112 Rule::Size(_) => match kind {
113 SizeKind::Numeric => "The :attribute field must be :size.",
114 SizeKind::String => "The :attribute field must be :size characters.",
115 SizeKind::Array => "The :attribute field must contain :size items.",
116 },
117 Rule::In(_) | Rule::NotIn(_) => "The selected :attribute is invalid.",
118 Rule::Confirmed => "The :attribute field confirmation does not match.",
119 Rule::Same(_) => "The :attribute field must match :other.",
120 Rule::Different(_) => "The :attribute field and :other must be different.",
121 Rule::Alpha => "The :attribute field must only contain letters.",
122 Rule::AlphaNum => "The :attribute field must only contain letters and numbers.",
123 Rule::AlphaDash => {
124 "The :attribute field must only contain letters, numbers, dashes, and underscores."
125 }
126 Rule::StartsWith(_) => "The :attribute field must start with one of the following: :values.",
127 Rule::EndsWith(_) => "The :attribute field must end with one of the following: :values.",
128 Rule::Date => "The :attribute field must be a valid date in the format YYYY-MM-DD.",
129 Rule::Uuid => "The :attribute field must be a valid UUID.",
130 Rule::Array => "The :attribute field must be an array.",
131 }
132}
133
134pub fn interpolate(template: &str, values: &[(&str, String)]) -> String {
137 let mut out = template.to_string();
138 for (name, value) in values {
139 out = out.replace(&format!(":{name}"), value);
140 }
141 out
142}
143
144pub fn format_number(value: f64) -> String {
146 if value.is_finite() && value.fract() == 0.0 && value.abs() < 1e15 {
147 format!("{}", value as i64)
148 } else {
149 format!("{value}")
150 }
151}
152
153pub fn format_values(values: &[String]) -> String {
155 values.join(", ")
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn a_field_name_reads_as_words_by_default() {
164 let messages = Messages::new();
165 assert_eq!(messages.label("email"), "email");
166 assert_eq!(messages.label("email_address"), "email address");
167 assert_eq!(messages.label("billing.postal-code"), "billing postal code");
168 }
169
170 #[test]
171 fn an_attribute_override_replaces_the_derived_label() {
172 let messages = Messages::new().attribute("dob", "date of birth");
173 assert_eq!(messages.label("dob"), "date of birth");
174 assert_eq!(messages.label("other"), "other");
175 }
176
177 #[test]
178 fn a_field_override_beats_a_rule_override_which_beats_the_default() {
179 let messages = Messages::new()
180 .with("required", "We need :attribute.")
181 .with("email.required", "An email address is required.");
182
183 assert_eq!(
184 messages.template("email", &Rule::Required, SizeKind::String),
185 "An email address is required."
186 );
187 assert_eq!(messages.template("name", &Rule::Required, SizeKind::String), "We need :attribute.");
188 assert_eq!(
189 messages.template("name", &Rule::Email, SizeKind::String),
190 "The :attribute field must be a valid email address."
191 );
192 }
193
194 #[test]
195 fn a_size_rule_picks_its_wording_from_the_kind_of_value() {
196 assert_eq!(
197 default_template(&Rule::Min(3.0), SizeKind::String),
198 "The :attribute field must be at least :min characters."
199 );
200 assert_eq!(
201 default_template(&Rule::Min(3.0), SizeKind::Numeric),
202 "The :attribute field must be at least :min."
203 );
204 assert_eq!(
205 default_template(&Rule::Min(3.0), SizeKind::Array),
206 "The :attribute field must have at least :min items."
207 );
208 }
209
210 #[test]
211 fn placeholders_are_interpolated_and_unknown_ones_are_left_visible() {
212 let rendered = interpolate(
213 "The :attribute field must be between :min and :max. :nope",
214 &[("attribute", "age".into()), ("min", "1".into()), ("max", "10".into())],
215 );
216 assert_eq!(rendered, "The age field must be between 1 and 10. :nope");
217 }
218
219 #[test]
220 fn bounds_render_without_a_decimal_point() {
221 assert_eq!(format_number(255.0), "255");
222 assert_eq!(format_number(1.5), "1.5");
223 }
224
225 #[test]
226 fn list_parameters_render_comma_separated() {
227 assert_eq!(format_values(&["a".to_string(), "b".to_string()]), "a, b");
228 }
229}