shared_framework/validation/
mod.rs1use regex::Regex;
20use serde_json::Value;
21use thiserror::Error;
22
23#[derive(Debug, Error)]
25#[error("validation failed on field '{field}': {message}")]
26pub struct ValidationException {
27 pub field: String,
29 pub message: String,
31}
32
33impl ValidationException {
34 pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
36 Self { field: field.into(), message: message.into() }
37 }
38}
39
40pub trait Validate {
42 fn validate(&self) -> Result<(), ValidationException>;
44}
45
46pub fn is_uuid(field: &str, value: &str, custom: Option<&str>) -> Result<(), ValidationException> {
50 if uuid::Uuid::parse_str(value).is_err() {
51 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a valid UUID but was '{value}'")).to_string()));
52 }
53 Ok(())
54}
55
56pub fn is_email(field: &str, value: &str, custom: Option<&str>) -> Result<(), ValidationException> {
58 static RE: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(|| {
59 Regex::new(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$").unwrap()
60 });
61 if !RE.is_match(value) {
62 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a valid email but was '{value}'")).to_string()));
63 }
64 Ok(())
65}
66
67pub fn is_match(field: &str, value: &str, pattern: &str, custom: Option<&str>) -> Result<(), ValidationException> {
69 let re = Regex::new(pattern).map_err(|e| ValidationException::new(field, e.to_string()))?;
70 if !re.is_match(value) {
71 return Err(ValidationException::new(field, custom.unwrap_or(&format!("value '{value}' does not match pattern {pattern}")).to_string()));
72 }
73 Ok(())
74}
75
76pub fn is_not_empty(field: &str, value: &Value, custom: Option<&str>) -> Result<(), ValidationException> {
78 let empty = match value {
79 Value::Null => true,
80 Value::String(s) => s.is_empty(),
81 Value::Array(a) => a.is_empty(),
82 Value::Object(o) => o.is_empty(),
83 _ => false,
84 };
85 if empty {
86 return Err(ValidationException::new(field, custom.unwrap_or("expected a non-empty value but it was empty").to_string()));
87 }
88 Ok(())
89}
90
91pub fn is_not_blank(field: &str, value: &str, custom: Option<&str>) -> Result<(), ValidationException> {
93 if value.trim().is_empty() {
94 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a non-blank string but was '{value}'")).to_string()));
95 }
96 Ok(())
97}
98
99pub fn is_length(field: &str, value: &str, min: usize, max: usize, custom: Option<&str>) -> Result<(), ValidationException> {
101 let len = value.len();
102 if len < min || len > max {
103 return Err(ValidationException::new(field, custom.unwrap_or(&format!("length {len} not within [{min}, {max}]")).to_string()));
104 }
105 Ok(())
106}
107
108pub fn is_size(field: &str, len: usize, min: usize, max: usize, custom: Option<&str>) -> Result<(), ValidationException> {
110 if len < min || len > max {
111 return Err(ValidationException::new(field, custom.unwrap_or(&format!("size {len} not within [{min}, {max}]")).to_string()));
112 }
113 Ok(())
114}
115
116pub fn is_in(field: &str, value: &str, allowed: &[&str], custom: Option<&str>) -> Result<(), ValidationException> {
118 if !allowed.contains(&value) {
119 return Err(ValidationException::new(field, custom.unwrap_or(&format!("value '{value}' is not one of {:?}", allowed)).to_string()));
120 }
121 Ok(())
122}
123
124pub fn is_url(field: &str, value: &str, custom: Option<&str>) -> Result<(), ValidationException> {
126 let url = url::Url::parse(value).map_err(|_| ValidationException::new(field, custom.unwrap_or(&format!("expected a valid URL but was '{value}'")).to_string()))?;
127 if url.scheme().is_empty() || url.host().is_none() {
128 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a valid URL but was '{value}'")).to_string()));
129 }
130 Ok(())
131}
132
133pub fn is_positive(field: &str, n: f64, custom: Option<&str>) -> Result<(), ValidationException> {
135 if n <= 0.0 {
136 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a positive value but was {n}")).to_string()));
137 }
138 Ok(())
139}
140
141pub fn is_negative(field: &str, n: f64, custom: Option<&str>) -> Result<(), ValidationException> {
143 if n >= 0.0 {
144 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a negative value but was {n}")).to_string()));
145 }
146 Ok(())
147}
148
149pub fn is_greater(field: &str, n: f64, bound: f64, or_equals: bool, custom: Option<&str>) -> Result<(), ValidationException> {
151 let ok = if or_equals { n >= bound } else { n > bound };
152 if !ok {
153 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected value >{} {bound} but was {n}", if or_equals { "=" } else { "" })).to_string()));
154 }
155 Ok(())
156}
157
158pub fn is_lesser(field: &str, n: f64, bound: f64, or_equals: bool, custom: Option<&str>) -> Result<(), ValidationException> {
160 let ok = if or_equals { n <= bound } else { n < bound };
161 if !ok {
162 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected value <{} {bound} but was {n}", if or_equals { "=" } else { "" })).to_string()));
163 }
164 Ok(())
165}
166
167pub fn is_between(field: &str, n: f64, start: f64, end: f64, inclusive: bool, custom: Option<&str>) -> Result<(), ValidationException> {
169 let ok = if inclusive { n >= start && n <= end } else { n > start && n < end };
170 if !ok {
171 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected value within [{start}, {end}] {} but was {n}", if inclusive { "inclusive" } else { "exclusive" })).to_string()));
172 }
173 Ok(())
174}
175
176pub fn is_outside(field: &str, n: f64, start: f64, end: f64, inclusive: bool, custom: Option<&str>) -> Result<(), ValidationException> {
178 let ok = if inclusive { n < start || n > end } else { n <= start || n >= end };
179 if !ok {
180 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected value outside [{start}, {end}] {} but was {n}", if inclusive { "inclusive" } else { "exclusive" })).to_string()));
181 }
182 Ok(())
183}
184
185pub fn is_before(field: &str, value: &chrono::DateTime<chrono::Utc>, bound: &chrono::DateTime<chrono::Utc>, custom: Option<&str>) -> Result<(), ValidationException> {
187 if !value.lt(bound) {
188 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a date strictly before {bound} but was {value}")).to_string()));
189 }
190 Ok(())
191}
192
193pub fn is_after(field: &str, value: &chrono::DateTime<chrono::Utc>, bound: &chrono::DateTime<chrono::Utc>, custom: Option<&str>) -> Result<(), ValidationException> {
195 if !value.gt(bound) {
196 return Err(ValidationException::new(field, custom.unwrap_or(&format!("expected a date strictly after {bound} but was {value}")).to_string()));
197 }
198 Ok(())
199}
200
201pub fn combine_or(field: &str, validators: &[Box<dyn Fn() -> Result<(), ValidationException> + Send + Sync>], custom: Option<&str>) -> Result<(), ValidationException> {
205 let mut errors = String::new();
206 for v in validators {
207 match v() {
208 Ok(_) => return Ok(()),
209 Err(e) => errors.push_str(&format!("; {}", e.message)),
210 }
211 }
212 Err(ValidationException::new(field, custom.unwrap_or(&format!("value did not satisfy any of {} combined constraints:{errors}", validators.len())).to_string()))
213}
214
215pub fn combine_and(validators: &[Box<dyn Fn() -> Result<(), ValidationException> + Send + Sync>]) -> Result<(), ValidationException> {
217 for v in validators {
218 v()?;
219 }
220 Ok(())
221}
222
223pub fn combine_not(field: &str, validators: &[Box<dyn Fn() -> Result<(), ValidationException> + Send + Sync>], custom: Option<&str>) -> Result<(), ValidationException> {
225 let mut all_passed = true;
226 for v in validators {
227 if v().is_err() {
228 all_passed = false;
229 break;
230 }
231 }
232 if all_passed && !validators.is_empty() {
233 return Err(ValidationException::new(field, custom.unwrap_or("value satisfied constraints that must not hold").to_string()));
234 }
235 Ok(())
236}
237
238pub fn validate<T: Validate>(target: &T) -> Result<(), ValidationException> {
240 target.validate()
241}