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> {
211 self.validate(input)?;
212
213 let mut kept = Map::new();
214 for (pattern, _) in &self.rules {
215 let root = pattern.split(['.', '*']).next().unwrap_or(pattern);
218 if root.is_empty() || kept.contains_key(root) {
219 continue;
220 }
221 if let Some(value) = input.get(root) {
222 kept.insert(root.to_string(), value.clone());
223 }
224 }
225 Ok(Value::Object(kept))
226 }
227
228 fn message_for(&self, pattern: &str, rule: &Rule, default: String) -> String {
229 let key = format!("{pattern}.{}", rule.name());
230 self.messages
231 .iter()
232 .find(|(candidate, _)| *candidate == key)
233 .map(|(_, message)| message.clone())
234 .unwrap_or(default)
235 }
236}
237
238fn expand(pattern: &str, input: &Value) -> Vec<String> {
243 if !pattern.contains('*') {
244 return vec![pattern.to_string()];
245 }
246
247 let mut expanded = vec![String::new()];
248 for segment in pattern.split('.') {
249 let mut next = Vec::new();
250 for prefix in &expanded {
251 if segment != "*" {
252 next.push(join(prefix, segment));
253 continue;
254 }
255 let Some(Value::Array(items)) = crate::lookup(input, prefix) else {
259 continue;
260 };
261 for index in 0..items.len() {
262 next.push(join(prefix, &index.to_string()));
263 }
264 }
265 expanded = next;
266 }
267 expanded
268}
269
270fn join(prefix: &str, segment: &str) -> String {
271 if prefix.is_empty() {
272 segment.to_string()
273 } else {
274 format!("{prefix}.{segment}")
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use serde_json::json;
282
283 fn validator() -> Validator {
284 Validator::new()
285 .rules("name", vec![Rule::Required, Rule::String, Rule::Max(10.0)])
286 .rules("email", vec![Rule::Required, Rule::Email])
287 .rules("age", vec![Rule::Integer, Rule::Min(18.0)])
288 }
289
290 #[test]
291 fn valid_input_passes() {
292 let input = json!({ "name": "Ada", "email": "ada@example.com", "age": "36" });
293 assert!(validator().validate(&input).is_ok());
294 }
295
296 #[test]
297 fn optional_fields_may_be_absent() {
298 let input = json!({ "name": "Ada", "email": "ada@example.com" });
299 assert!(validator().validate(&input).is_ok());
300 }
301
302 #[test]
303 fn collects_a_failure_per_field() {
304 let input = json!({ "email": "nope", "age": "12" });
305 let errors = validator().validate(&input).unwrap_err();
306
307 assert_eq!(errors.len(), 3);
308 assert!(errors.has("name"));
309 assert!(errors.has("email"));
310 assert!(errors.has("age"));
311 }
312
313 #[test]
314 fn a_failed_required_rule_silences_the_rest_for_that_field() {
315 let input = json!({ "email": "" });
316 let errors = validator().validate(&input).unwrap_err();
317
318 assert_eq!(errors.get("email").len(), 1);
320 assert!(errors.first("email").unwrap().contains("required"));
321 }
322
323 #[test]
324 fn non_presence_failures_accumulate() {
325 let validator = Validator::new().rules("code", vec![Rule::Alpha, Rule::Min(5.0)]);
326 let errors = validator.validate(&json!({ "code": "a1" })).unwrap_err();
327 assert_eq!(errors.get("code").len(), 2);
328 }
329
330 #[test]
331 fn custom_messages_replace_the_default() {
332 let validator = validator().custom_message("email.required", "We need your email.");
333 let errors = validator.validate(&json!({ "name": "Ada" })).unwrap_err();
334 assert_eq!(errors.first("email"), Some("We need your email."));
335 }
336
337 #[test]
338 fn a_custom_message_only_replaces_its_own_rule() {
339 let validator = validator().custom_message("email.required", "We need your email.");
340 let errors = validator.validate(&json!({ "name": "Ada", "email": "x" })).unwrap_err();
341 assert!(errors.first("email").unwrap().contains("valid email address"));
342 }
343
344 #[test]
345 fn dotted_paths_reach_into_objects() {
346 let validator = Validator::new().rules("address.city", vec![Rule::Required]);
347 assert!(validator.validate(&json!({ "address": { "city": "Paris" } })).is_ok());
348
349 let errors = validator.validate(&json!({ "address": {} })).unwrap_err();
350 assert!(errors.has("address.city"));
351 }
352
353 #[test]
354 fn wildcards_fan_out_over_arrays() {
355 let validator = Validator::new()
356 .rules("items.*.quantity", vec![Rule::Required, Rule::Integer, Rule::Min(1.0)]);
357
358 let input = json!({ "items": [{ "quantity": "2" }, { "quantity": "0" }, {}] });
359 let errors = validator.validate(&input).unwrap_err();
360
361 assert!(!errors.has("items.0.quantity"));
362 assert!(errors.has("items.1.quantity"), "0 is below the minimum");
363 assert!(errors.has("items.2.quantity"), "missing entirely");
364 }
365
366 #[test]
367 fn a_wildcard_over_a_missing_array_produces_no_failures() {
368 let validator = Validator::new().rules("items.*.quantity", vec![Rule::Required]);
369 assert!(validator.validate(&json!({})).is_ok());
370 }
371
372 #[test]
373 fn nested_wildcards_expand() {
374 let validator = Validator::new().rules("orders.*.items.*.sku", vec![Rule::Required]);
375 let input = json!({
376 "orders": [
377 { "items": [{ "sku": "a" }, {}] },
378 { "items": [{ "sku": "c" }] }
379 ]
380 });
381 let errors = validator.validate(&input).unwrap_err();
382 assert_eq!(errors.fields(), vec!["orders.0.items.1.sku"]);
383 }
384
385 #[test]
386 fn validated_only_drops_unvalidated_keys() {
387 let input = json!({
388 "name": "Ada",
389 "email": "ada@example.com",
390 "is_admin": true
391 });
392 let kept = validator().validated_only(&input).unwrap();
393
394 assert_eq!(kept.get("name"), Some(&json!("Ada")));
395 assert!(kept.get("is_admin").is_none(), "mass assignment must not slip through");
396 }
397
398 #[test]
399 fn validated_only_keeps_the_parent_of_a_nested_rule() {
400 let validator = Validator::new()
401 .rules("address.city", vec![Rule::Required])
402 .rules("items.*.sku", vec![Rule::Required]);
403
404 let input = json!({
405 "address": { "city": "Paris", "postcode": "75001" },
406 "items": [{ "sku": "a" }],
407 "sneaky": 1
408 });
409 let kept = validator.validated_only(&input).unwrap();
410
411 assert!(kept.get("address").is_some());
412 assert!(kept.get("items").is_some());
413 assert!(kept.get("sneaky").is_none());
414 }
415
416 #[test]
417 fn errors_render_as_a_stable_json_payload() {
418 let errors = validator().validate(&json!({})).unwrap_err();
419 let payload = errors.to_json();
420
421 let keys: Vec<&String> = payload.as_object().unwrap().keys().collect();
423 assert_eq!(keys, vec!["email", "name"]);
424 assert!(payload["name"][0].as_str().unwrap().contains("required"));
425 }
426
427 #[test]
428 fn errors_convert_into_a_422() {
429 let errors = validator().validate(&json!({})).unwrap_err();
430 let error: Error = errors.into();
431 assert_eq!(error.status(), 422);
432 assert!(error.details().is_some());
433 }
434
435 #[test]
436 fn error_accessors() {
437 let errors = validator().validate(&json!({})).unwrap_err();
438 assert!(!errors.is_empty());
439 assert!(errors.first_message().is_some());
440 assert_eq!(errors.all_messages().len(), 2);
441 assert!(errors.get("nope").is_empty());
442 assert!(errors.first("nope").is_none());
443 }
444
445 #[test]
446 fn from_rules_builds_from_a_rule_set() {
447 let rules: RuleSet = vec![("title", vec![Rule::Required])];
448 let validator = Validator::from_rules(rules);
449 assert!(validator.validate(&json!({})).is_err());
450 assert!(validator.validate(&json!({ "title": "x" })).is_ok());
451 }
452}