use std::{
borrow::Cow,
collections::HashMap,
fmt::{self, Write},
sync::LazyLock,
};
use validator::{ValidationError, ValidationErrors, ValidationErrorsKind};
#[cfg(any(test, feature = "test-utils"))]
use crate::domain::Command;
#[cfg(any(test, feature = "test-utils"))]
use serde::de::DeserializeOwned;
#[cfg(any(test, feature = "test-utils"))]
use validator::Validate;
static CONFIG: LazyLock<config::Config> = LazyLock::new(|| crate::config::i18n_config());
static VALIDATION: LazyLock<HashMap<String, HashMap<String, String>>> =
LazyLock::new(|| crate::config::load_named_setting(&CONFIG, "validation"));
#[allow(dead_code)]
pub(crate) static RESPONSE: LazyLock<HashMap<String, HashMap<String, String>>> =
LazyLock::new(|| crate::config::load_named_setting(&CONFIG, "response"));
#[doc(hidden)]
#[cfg(any(test, feature = "test-utils"))]
pub fn validate<T>(com: &T, lang: &str) -> Result<(), String>
where
T: Command + Validate + DeserializeOwned,
{
if let Err(e) = com.validate() {
let mut result = String::new();
return match validation(&mut result, &e, lang) {
Ok(()) => Err(result),
Err(_) => Err(e.to_string()),
};
}
Ok(())
}
pub(crate) fn validation(result: &mut String, e: &ValidationErrors, lang: &str) -> fmt::Result {
let errors = e.errors();
for (idx, (path, errs)) in errors.iter().enumerate() {
display_errors(result, errs, path, lang)?;
if idx + 1 < errors.len() {
result.push('\n');
}
}
Ok(())
}
fn display_errors(
result: &mut String,
errs: &ValidationErrorsKind,
path: &str,
lang: &str,
) -> fmt::Result {
fn display_struct(
result: &mut String,
errs: &ValidationErrors,
path: &str,
lang: &str,
) -> fmt::Result {
let mut full_path = String::new();
full_path.push_str(path);
full_path.push('.');
let base_len = full_path.len();
for (path, err) in errs.errors() {
full_path.push_str(path);
display_errors(result, err, &full_path, lang)?;
result.push('\n');
full_path.truncate(base_len);
}
Ok(())
}
match errs {
ValidationErrorsKind::Field(errs) => {
result.push_str(path);
result.push_str(": ");
let len = errs.len();
for (idx, err) in errs.iter().enumerate() {
if idx + 1 == len {
result.push_str(&render_template(err, lang));
} else {
result.push_str(&render_template(err, lang));
result.push_str(", ");
}
}
}
ValidationErrorsKind::Struct(errs) => display_struct(result, errs, path, lang)?,
ValidationErrorsKind::List(errs) => {
let mut full_path = String::new();
full_path.push_str(path);
let base_len = full_path.len();
for (idx, err) in errs.iter() {
write!(&mut full_path, "[{}]", idx)?;
display_struct(result, err, &full_path, lang)?;
full_path.truncate(base_len);
}
}
}
Ok(())
}
fn render_template(err: &ValidationError, lang: &str) -> Cow<'static, str> {
let template = match VALIDATION
.get(lang)
.or(VALIDATION.get("zh"))
.and_then(|l| l.get(err.code.as_ref()))
{
Some(template) => template,
None => return err.code.clone(),
};
let mut result = String::with_capacity(template.len() + 64);
let mut pos = 0;
while let Some(open_brace) = template[pos..].find('{') {
let start = pos + open_brace;
if let Some(close_brace) = template[start + 1..].find('}') {
let placeholder_end = start + 1 + close_brace;
let placeholder = &template[start + 1..placeholder_end];
result.push_str(&template[pos..start]);
match err.params.get(placeholder) {
Some(value) => result.push_str(&value.to_string()),
None => result.push_str(&template[start..=placeholder_end]),
}
pos = placeholder_end + 1;
}
}
if pos == 0 {
return Cow::Borrowed(template);
}
if pos < template.len() {
result.push_str(&template[pos..]);
}
Cow::Owned(result)
}