use crate::rule::Rule;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SizeKind {
Numeric,
String,
Array,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Messages {
overrides: BTreeMap<String, String>,
attributes: BTreeMap<String, String>,
}
impl Messages {
pub fn new() -> Self {
Messages::default()
}
pub fn set(&mut self, key: impl Into<String>, message: impl Into<String>) {
self.overrides.insert(key.into(), message.into());
}
pub fn with(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
self.set(key, message);
self
}
pub fn set_attribute(&mut self, field: impl Into<String>, label: impl Into<String>) {
self.attributes.insert(field.into(), label.into());
}
pub fn attribute(mut self, field: impl Into<String>, label: impl Into<String>) -> Self {
self.set_attribute(field, label);
self
}
pub fn label(&self, field: &str) -> String {
match self.attributes.get(field) {
Some(label) => label.clone(),
None => field.replace(['_', '-', '.'], " "),
}
}
pub fn template(&self, field: &str, rule: &Rule, kind: SizeKind) -> String {
let name = rule.name();
self.overrides
.get(&format!("{field}.{name}"))
.or_else(|| self.overrides.get(name))
.cloned()
.unwrap_or_else(|| default_template(rule, kind).to_string())
}
}
pub fn default_template(rule: &Rule, kind: SizeKind) -> &'static str {
match rule {
Rule::Required => "The :attribute field is required.",
Rule::Nullable => "The :attribute field is invalid.",
Rule::String => "The :attribute field must be a string.",
Rule::Integer => "The :attribute field must be an integer.",
Rule::Numeric => "The :attribute field must be a number.",
Rule::Boolean => "The :attribute field must be true or false.",
Rule::Email => "The :attribute field must be a valid email address.",
Rule::Url => "The :attribute field must be a valid URL.",
Rule::Min(_) => match kind {
SizeKind::Numeric => "The :attribute field must be at least :min.",
SizeKind::String => "The :attribute field must be at least :min characters.",
SizeKind::Array => "The :attribute field must have at least :min items.",
},
Rule::Max(_) => match kind {
SizeKind::Numeric => "The :attribute field must not be greater than :max.",
SizeKind::String => "The :attribute field must not be greater than :max characters.",
SizeKind::Array => "The :attribute field must not have more than :max items.",
},
Rule::Between(_, _) => match kind {
SizeKind::Numeric => "The :attribute field must be between :min and :max.",
SizeKind::String => "The :attribute field must be between :min and :max characters.",
SizeKind::Array => "The :attribute field must have between :min and :max items.",
},
Rule::Size(_) => match kind {
SizeKind::Numeric => "The :attribute field must be :size.",
SizeKind::String => "The :attribute field must be :size characters.",
SizeKind::Array => "The :attribute field must contain :size items.",
},
Rule::In(_) | Rule::NotIn(_) => "The selected :attribute is invalid.",
Rule::Confirmed => "The :attribute field confirmation does not match.",
Rule::Same(_) => "The :attribute field must match :other.",
Rule::Different(_) => "The :attribute field and :other must be different.",
Rule::Alpha => "The :attribute field must only contain letters.",
Rule::AlphaNum => "The :attribute field must only contain letters and numbers.",
Rule::AlphaDash => {
"The :attribute field must only contain letters, numbers, dashes, and underscores."
}
Rule::StartsWith(_) => "The :attribute field must start with one of the following: :values.",
Rule::EndsWith(_) => "The :attribute field must end with one of the following: :values.",
Rule::Date => "The :attribute field must be a valid date in the format YYYY-MM-DD.",
Rule::Uuid => "The :attribute field must be a valid UUID.",
Rule::Array => "The :attribute field must be an array.",
}
}
pub fn interpolate(template: &str, values: &[(&str, String)]) -> String {
let mut out = template.to_string();
for (name, value) in values {
out = out.replace(&format!(":{name}"), value);
}
out
}
pub fn format_number(value: f64) -> String {
if value.is_finite() && value.fract() == 0.0 && value.abs() < 1e15 {
format!("{}", value as i64)
} else {
format!("{value}")
}
}
pub fn format_values(values: &[String]) -> String {
values.join(", ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_field_name_reads_as_words_by_default() {
let messages = Messages::new();
assert_eq!(messages.label("email"), "email");
assert_eq!(messages.label("email_address"), "email address");
assert_eq!(messages.label("billing.postal-code"), "billing postal code");
}
#[test]
fn an_attribute_override_replaces_the_derived_label() {
let messages = Messages::new().attribute("dob", "date of birth");
assert_eq!(messages.label("dob"), "date of birth");
assert_eq!(messages.label("other"), "other");
}
#[test]
fn a_field_override_beats_a_rule_override_which_beats_the_default() {
let messages = Messages::new()
.with("required", "We need :attribute.")
.with("email.required", "An email address is required.");
assert_eq!(
messages.template("email", &Rule::Required, SizeKind::String),
"An email address is required."
);
assert_eq!(messages.template("name", &Rule::Required, SizeKind::String), "We need :attribute.");
assert_eq!(
messages.template("name", &Rule::Email, SizeKind::String),
"The :attribute field must be a valid email address."
);
}
#[test]
fn a_size_rule_picks_its_wording_from_the_kind_of_value() {
assert_eq!(
default_template(&Rule::Min(3.0), SizeKind::String),
"The :attribute field must be at least :min characters."
);
assert_eq!(
default_template(&Rule::Min(3.0), SizeKind::Numeric),
"The :attribute field must be at least :min."
);
assert_eq!(
default_template(&Rule::Min(3.0), SizeKind::Array),
"The :attribute field must have at least :min items."
);
}
#[test]
fn placeholders_are_interpolated_and_unknown_ones_are_left_visible() {
let rendered = interpolate(
"The :attribute field must be between :min and :max. :nope",
&[("attribute", "age".into()), ("min", "1".into()), ("max", "10".into())],
);
assert_eq!(rendered, "The age field must be between 1 and 10. :nope");
}
#[test]
fn bounds_render_without_a_decimal_point() {
assert_eq!(format_number(255.0), "255");
assert_eq!(format_number(1.5), "1.5");
}
#[test]
fn list_parameters_render_comma_separated() {
assert_eq!(format_values(&["a".to_string(), "b".to_string()]), "a, b");
}
}