1use std::collections::BTreeMap;
4
5use rainier_support::Error;
6use serde_json::{Map, Value};
7
8use crate::rule::{Outcome, Rule};
9
10#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct ValidationErrors {
18 errors: BTreeMap<String, Vec<String>>,
19}
20
21impl ValidationErrors {
22 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
29 self.errors.entry(field.into()).or_default().push(message.into());
30 }
31
32 pub fn is_empty(&self) -> bool {
34 self.errors.is_empty()
35 }
36
37 pub fn len(&self) -> usize {
39 self.errors.len()
40 }
41
42 pub fn has(&self, field: &str) -> bool {
44 self.errors.contains_key(field)
45 }
46
47 pub fn get(&self, field: &str) -> &[String] {
49 self.errors.get(field).map(Vec::as_slice).unwrap_or(&[])
50 }
51
52 pub fn first(&self, field: &str) -> Option<&str> {
54 self.errors.get(field)?.first().map(String::as_str)
55 }
56
57 pub fn first_message(&self) -> Option<&str> {
59 self.errors.values().next()?.first().map(String::as_str)
60 }
61
62 pub fn all_messages(&self) -> Vec<&str> {
64 self.errors.values().flatten().map(String::as_str).collect()
65 }
66
67 pub fn fields(&self) -> Vec<&str> {
69 self.errors.keys().map(String::as_str).collect()
70 }
71
72 pub fn to_json(&self) -> Value {
74 let mut map = Map::new();
75 for (field, messages) in &self.errors {
76 map.insert(
77 field.clone(),
78 Value::Array(messages.iter().cloned().map(Value::String).collect()),
79 );
80 }
81 Value::Object(map)
82 }
83}
84
85impl From<ValidationErrors> for Error {
86 fn from(errors: ValidationErrors) -> Self {
89 Error::validation(errors.to_json())
90 }
91}
92
93impl std::fmt::Display for ValidationErrors {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self.first_message() {
96 Some(message) => f.write_str(message),
97 None => f.write_str("The given data was valid."),
98 }
99 }
100}
101
102pub type RuleSet = Vec<(&'static str, Vec<Rule>)>;
104
105#[derive(Debug, Default)]
120pub struct Validator {
121 rules: Vec<(String, Vec<Rule>)>,
122 messages: Vec<(String, String)>,
123}
124
125impl Validator {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn from_rules(rules: RuleSet) -> Self {
133 Self {
134 rules: rules.into_iter().map(|(field, rules)| (field.to_string(), rules)).collect(),
135 messages: Vec::new(),
136 }
137 }
138
139 pub fn rules(mut self, field: impl Into<String>, rules: Vec<Rule>) -> Self {
144 self.rules.push((field.into(), rules));
145 self
146 }
147
148 pub fn custom_message(mut self, key: impl Into<String>, message: impl Into<String>) -> Self {
151 self.messages.push((key.into(), message.into()));
152 self
153 }
154
155 pub fn custom_messages(
157 mut self,
158 messages: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
159 ) -> Self {
160 self.messages
161 .extend(messages.into_iter().map(|(key, message)| (key.into(), message.into())));
162 self
163 }
164
165 pub fn validate(&self, input: &Value) -> Result<(), ValidationErrors> {
167 let mut errors = ValidationErrors::new();
168
169 for (pattern, rules) in &self.rules {
170 for field in expand(pattern, input) {
171 let value = crate::lookup(input, &field);
172
173 for rule in rules {
174 match rule.check(&field, value, input) {
175 Outcome::Pass => {}
176 Outcome::Fail(message) => {
177 errors.add(&field, self.message_for(pattern, rule, message));
178 if rule.applies_to_missing() {
182 break;
183 }
184 }
185 Outcome::Skip => break,
187 }
188 }
189 }
190 }
191
192 if errors.is_empty() {
193 Ok(())
194 } else {
195 Err(errors)
196 }
197 }
198
199 pub fn validated(&self, input: &Value) -> Result<Value, ValidationErrors> {
201 self.validate(input)?;
202 Ok(input.clone())
203 }
204
205 pub fn validated_only(&self, input: &Value) -> Result<Value, ValidationErrors> {
212 self.validate(input)?;
213
214 let mut kept = Map::new();
215 for (pattern, _) in &self.rules {
216 let root = pattern.split(['.', '*']).next().unwrap_or(pattern);
219 if root.is_empty() || kept.contains_key(root) {
220 continue;
221 }
222 if let Some(value) = input.get(root) {
223 kept.insert(root.to_string(), value.clone());
224 }
225 }
226 Ok(Value::Object(kept))
227 }
228
229 fn message_for(&self, pattern: &str, rule: &Rule, default: String) -> String {
230 let key = format!("{pattern}.{}", rule.name());
231 self.messages
232 .iter()
233 .find(|(candidate, _)| *candidate == key)
234 .map(|(_, message)| message.clone())
235 .unwrap_or(default)
236 }
237}
238
239fn expand(pattern: &str, input: &Value) -> Vec<String> {
244 if !pattern.contains('*') {
245 return vec![pattern.to_string()];
246 }
247
248 let mut expanded = vec![String::new()];
249 for segment in pattern.split('.') {
250 let mut next = Vec::new();
251 for prefix in &expanded {
252 if segment != "*" {
253 next.push(join(prefix, segment));
254 continue;
255 }
256 let Some(Value::Array(items)) = crate::lookup(input, prefix) else {
260 continue;
261 };
262 for index in 0..items.len() {
263 next.push(join(prefix, &index.to_string()));
264 }
265 }
266 expanded = next;
267 }
268 expanded
269}
270
271fn join(prefix: &str, segment: &str) -> String {
272 if prefix.is_empty() {
273 segment.to_string()
274 } else {
275 format!("{prefix}.{segment}")
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use serde_json::json;
283
284 fn validator() -> Validator {
285 Validator::new()
286 .rules("name", vec![Rule::Required, Rule::String, Rule::Max(10.0)])
287 .rules("email", vec![Rule::Required, Rule::Email])
288 .rules("age", vec![Rule::Integer, Rule::Min(18.0)])
289 }
290
291 #[test]
292 fn valid_input_passes() {
293 let input = json!({ "name": "Ada", "email": "ada@example.com", "age": "36" });
294 assert!(validator().validate(&input).is_ok());
295 }
296
297 #[test]
298 fn optional_fields_may_be_absent() {
299 let input = json!({ "name": "Ada", "email": "ada@example.com" });
300 assert!(validator().validate(&input).is_ok());
301 }
302
303 #[test]
304 fn collects_a_failure_per_field() {
305 let input = json!({ "email": "nope", "age": "12" });
306 let errors = validator().validate(&input).unwrap_err();
307
308 assert_eq!(errors.len(), 3);
309 assert!(errors.has("name"));
310 assert!(errors.has("email"));
311 assert!(errors.has("age"));
312 }
313
314 #[test]
315 fn a_failed_required_rule_silences_the_rest_for_that_field() {
316 let input = json!({ "email": "" });
317 let errors = validator().validate(&input).unwrap_err();
318
319 assert_eq!(errors.get("email").len(), 1);
321 assert!(errors.first("email").unwrap().contains("required"));
322 }
323
324 #[test]
325 fn non_presence_failures_accumulate() {
326 let validator = Validator::new().rules("code", vec![Rule::Alpha, Rule::Min(5.0)]);
327 let errors = validator.validate(&json!({ "code": "a1" })).unwrap_err();
328 assert_eq!(errors.get("code").len(), 2);
329 }
330
331 #[test]
332 fn custom_messages_replace_the_default() {
333 let validator = validator().custom_message("email.required", "We need your email.");
334 let errors = validator.validate(&json!({ "name": "Ada" })).unwrap_err();
335 assert_eq!(errors.first("email"), Some("We need your email."));
336 }
337
338 #[test]
339 fn a_custom_message_only_replaces_its_own_rule() {
340 let validator = validator().custom_message("email.required", "We need your email.");
341 let errors = validator.validate(&json!({ "name": "Ada", "email": "x" })).unwrap_err();
342 assert!(errors.first("email").unwrap().contains("valid email address"));
343 }
344
345 #[test]
346 fn dotted_paths_reach_into_objects() {
347 let validator = Validator::new().rules("address.city", vec![Rule::Required]);
348 assert!(validator.validate(&json!({ "address": { "city": "Paris" } })).is_ok());
349
350 let errors = validator.validate(&json!({ "address": {} })).unwrap_err();
351 assert!(errors.has("address.city"));
352 }
353
354 #[test]
355 fn wildcards_fan_out_over_arrays() {
356 let validator = Validator::new()
357 .rules("items.*.quantity", vec![Rule::Required, Rule::Integer, Rule::Min(1.0)]);
358
359 let input = json!({ "items": [{ "quantity": "2" }, { "quantity": "0" }, {}] });
360 let errors = validator.validate(&input).unwrap_err();
361
362 assert!(!errors.has("items.0.quantity"));
363 assert!(errors.has("items.1.quantity"), "0 is below the minimum");
364 assert!(errors.has("items.2.quantity"), "missing entirely");
365 }
366
367 #[test]
368 fn a_wildcard_over_a_missing_array_produces_no_failures() {
369 let validator = Validator::new().rules("items.*.quantity", vec![Rule::Required]);
370 assert!(validator.validate(&json!({})).is_ok());
371 }
372
373 #[test]
374 fn nested_wildcards_expand() {
375 let validator = Validator::new().rules("orders.*.items.*.sku", vec![Rule::Required]);
376 let input = json!({
377 "orders": [
378 { "items": [{ "sku": "a" }, {}] },
379 { "items": [{ "sku": "c" }] }
380 ]
381 });
382 let errors = validator.validate(&input).unwrap_err();
383 assert_eq!(errors.fields(), vec!["orders.0.items.1.sku"]);
384 }
385
386 #[test]
387 fn validated_only_drops_unvalidated_keys() {
388 let input = json!({
389 "name": "Ada",
390 "email": "ada@example.com",
391 "is_admin": true
392 });
393 let kept = validator().validated_only(&input).unwrap();
394
395 assert_eq!(kept.get("name"), Some(&json!("Ada")));
396 assert!(kept.get("is_admin").is_none(), "mass assignment must not slip through");
397 }
398
399 #[test]
400 fn validated_only_keeps_the_parent_of_a_nested_rule() {
401 let validator = Validator::new()
402 .rules("address.city", vec![Rule::Required])
403 .rules("items.*.sku", vec![Rule::Required]);
404
405 let input = json!({
406 "address": { "city": "Paris", "postcode": "75001" },
407 "items": [{ "sku": "a" }],
408 "sneaky": 1
409 });
410 let kept = validator.validated_only(&input).unwrap();
411
412 assert!(kept.get("address").is_some());
413 assert!(kept.get("items").is_some());
414 assert!(kept.get("sneaky").is_none());
415 }
416
417 #[test]
418 fn errors_render_as_a_stable_json_payload() {
419 let errors = validator().validate(&json!({})).unwrap_err();
420 let payload = errors.to_json();
421
422 let keys: Vec<&String> = payload.as_object().unwrap().keys().collect();
424 assert_eq!(keys, vec!["email", "name"]);
425 assert!(payload["name"][0].as_str().unwrap().contains("required"));
426 }
427
428 #[test]
429 fn errors_convert_into_a_422() {
430 let errors = validator().validate(&json!({})).unwrap_err();
431 let error: Error = errors.into();
432 assert_eq!(error.status(), 422);
433 assert!(error.details().is_some());
434 }
435
436 #[test]
437 fn error_accessors() {
438 let errors = validator().validate(&json!({})).unwrap_err();
439 assert!(!errors.is_empty());
440 assert!(errors.first_message().is_some());
441 assert_eq!(errors.all_messages().len(), 2);
442 assert!(errors.get("nope").is_empty());
443 assert!(errors.first("nope").is_none());
444 }
445
446 #[test]
447 fn from_rules_builds_from_a_rule_set() {
448 let rules: RuleSet = vec![("title", vec![Rule::Required])];
449 let validator = Validator::from_rules(rules);
450 assert!(validator.validate(&json!({})).is_err());
451 assert!(validator.validate(&json!({ "title": "x" })).is_ok());
452 }
453}