1use std::ops::{Deref, DerefMut};
42
43#[cfg(feature = "with-db")]
44use sea_orm::DbErr;
45use serde::{Deserialize, Serialize};
46use validator::{Validate, ValidationError, ValidationErrors};
47
48#[derive(Debug, Deserialize, Serialize)]
52#[allow(clippy::module_name_repetitions)]
53pub struct ModelValidationMessage {
54 pub code: String,
55 pub message: Option<String>,
56}
57
58#[deprecated(
64 since = "0.15.1",
65 note = "Use the builtin email validator from `validator`"
66)]
67pub fn is_valid_email(email: &str) -> Result<(), ValidationError> {
68 if email.contains('@') {
69 Ok(())
70 } else {
71 Err(ValidationError::new("invalid email"))
72 }
73}
74
75#[derive(Debug, thiserror::Error)]
86#[error("Model validation failed: {0}")]
87pub struct ModelValidationErrors(ValidationErrors);
88
89impl Deref for ModelValidationErrors {
90 type Target = ValidationErrors;
91
92 fn deref(&self) -> &Self::Target {
93 &self.0
94 }
95}
96
97impl DerefMut for ModelValidationErrors {
98 fn deref_mut(&mut self) -> &mut Self::Target {
99 &mut self.0
100 }
101}
102
103#[cfg(feature = "with-db")]
104impl From<ModelValidationErrors> for DbErr {
105 fn from(errors: ModelValidationErrors) -> Self {
106 into_db_error(&errors)
107 }
108}
109
110#[cfg(feature = "with-db")]
111#[must_use]
112pub fn into_db_error(errors: &ModelValidationErrors) -> sea_orm::DbErr {
113 use std::collections::BTreeMap;
114
115 let errors = &errors.0;
116 let error_data: BTreeMap<String, Vec<ModelValidationMessage>> = errors
117 .field_errors()
118 .iter()
119 .map(|(field, field_errors)| {
120 let errors = field_errors
121 .iter()
122 .map(|err| ModelValidationMessage {
123 code: err.code.to_string(),
124 message: err.message.as_ref().map(std::string::ToString::to_string),
125 })
126 .collect();
127 ((*field).to_string(), errors)
128 })
129 .collect();
130 let json_errors = serde_json::to_value(error_data);
131 match json_errors {
132 Ok(errors_json) => sea_orm::DbErr::Custom(errors_json.to_string()),
133 Err(err) => sea_orm::DbErr::Custom(format!(
134 "[before_save] could not parse validation errors. err: {err}"
135 )),
136 }
137}
138
139pub trait Validatable {
142 fn validate(&self) -> Result<(), ModelValidationErrors> {
148 self.validator().validate().map_err(ModelValidationErrors)
149 }
150 fn validator(&self) -> Box<dyn Validate>;
151}
152
153#[cfg(test)]
154mod tests {
155
156 use insta::assert_debug_snapshot;
157 use rstest::rstest;
158 use serde::Deserialize;
159 use validator::Validate;
160
161 use super::*;
162
163 #[derive(Debug, Validate, Deserialize)]
164 pub struct TestValidator {
165 #[validate(length(min = 4, message = "Invalid min characters long."))]
166 pub name: String,
167 }
168
169 #[rstest]
170 #[case("test@example.com", true)]
171 #[case("invalid-email", false)]
172 fn can_validate_email(#[case] test_name: &str, #[case] expected: bool) {
173 assert_eq!(is_valid_email(test_name).is_ok(), expected);
174 }
175
176 #[cfg(feature = "with-db")]
177 #[rstest]
178 #[case("foo")]
179 #[case("foo-bar")]
180 fn can_validate_into_db_error(#[case] name: &str) {
181 let data = TestValidator {
182 name: name.to_string(),
183 };
184
185 assert_debug_snapshot!(
186 format!("struct-[{name}]"),
187 data.validate()
188 .map_err(|e| into_db_error(&ModelValidationErrors(e)))
189 );
190 }
191}