skp_validator_core/
error.rs

1//! Structured validation error types.
2//!
3//! Provides nested, path-aware validation errors that can represent:
4//! - Field-level errors
5//! - Nested struct errors
6//! - Collection (array/map) item errors
7//!
8//! # Error Structure
9//!
10//! ```text
11//! ValidationErrors
12//! ├── errors: Vec<ValidationError>        // Root-level errors
13//! └── fields: BTreeMap<String, FieldErrors>
14//!     ├── Simple(Vec<ValidationError>)    // Field errors
15//!     ├── Nested(Box<ValidationErrors>)   // Nested struct
16//!     ├── List(BTreeMap<usize, ...>)      // Array items
17//!     └── Map(BTreeMap<String, ...>)      // Map entries
18//! ```
19
20use crate::path::FieldPath;
21use std::collections::BTreeMap;
22use std::fmt;
23
24#[cfg(feature = "serde")]
25use serde::{Deserialize, Serialize};
26
27/// A single validation error with path, code, and message.
28///
29/// # Example
30///
31/// ```rust
32/// use skp_validator_core::{ValidationError, FieldPath};
33///
34/// let error = ValidationError::new("email", "email.invalid", "Must be a valid email address")
35///     .with_param("value", "invalid-email");
36/// ```
37#[derive(Debug, Clone)]
38#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
39pub struct ValidationError {
40    /// Path to the field that failed validation
41    pub path: FieldPath,
42
43    /// Error code for programmatic handling (e.g., "email.invalid", "length.min")
44    pub code: String,
45
46    /// Human-readable error message
47    pub message: String,
48
49    /// Additional parameters for error formatting and i18n
50    #[cfg_attr(
51        feature = "serde",
52        serde(default, skip_serializing_if = "BTreeMap::is_empty")
53    )]
54    pub params: BTreeMap<String, ErrorParam>,
55}
56
57/// Parameter value for validation errors (supports multiple types)
58#[derive(Debug, Clone)]
59#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
60#[cfg_attr(feature = "serde", serde(untagged))]
61pub enum ErrorParam {
62    /// String parameter
63    String(String),
64    /// Integer parameter
65    Int(i64),
66    /// Float parameter
67    Float(f64),
68    /// Boolean parameter
69    Bool(bool),
70    /// List of values
71    List(Vec<String>),
72}
73
74impl From<String> for ErrorParam {
75    fn from(s: String) -> Self {
76        Self::String(s)
77    }
78}
79
80impl From<&str> for ErrorParam {
81    fn from(s: &str) -> Self {
82        Self::String(s.to_string())
83    }
84}
85
86impl From<i64> for ErrorParam {
87    fn from(n: i64) -> Self {
88        Self::Int(n)
89    }
90}
91
92impl From<i32> for ErrorParam {
93    fn from(n: i32) -> Self {
94        Self::Int(n as i64)
95    }
96}
97
98impl From<usize> for ErrorParam {
99    fn from(n: usize) -> Self {
100        Self::Int(n as i64)
101    }
102}
103
104impl From<f64> for ErrorParam {
105    fn from(n: f64) -> Self {
106        Self::Float(n)
107    }
108}
109
110impl From<bool> for ErrorParam {
111    fn from(b: bool) -> Self {
112        Self::Bool(b)
113    }
114}
115
116impl From<Vec<String>> for ErrorParam {
117    fn from(v: Vec<String>) -> Self {
118        Self::List(v)
119    }
120}
121
122impl ValidationError {
123    /// Create a new validation error.
124    ///
125    /// # Arguments
126    ///
127    /// * `field` - The field name (will be converted to a FieldPath)
128    /// * `code` - Error code for programmatic handling
129    /// * `message` - Human-readable message
130    pub fn new(
131        field: impl Into<String>,
132        code: impl Into<String>,
133        message: impl Into<String>,
134    ) -> Self {
135        Self {
136            path: FieldPath::from_field(field),
137            code: code.into(),
138            message: message.into(),
139            params: BTreeMap::new(),
140        }
141    }
142
143    /// Create a new validation error with a full path.
144    pub fn with_path(
145        path: FieldPath,
146        code: impl Into<String>,
147        message: impl Into<String>,
148    ) -> Self {
149        Self {
150            path,
151            code: code.into(),
152            message: message.into(),
153            params: BTreeMap::new(),
154        }
155    }
156
157    /// Create a root-level error (empty path).
158    pub fn root(code: impl Into<String>, message: impl Into<String>) -> Self {
159        Self {
160            path: FieldPath::new(),
161            code: code.into(),
162            message: message.into(),
163            params: BTreeMap::new(),
164        }
165    }
166
167    /// Add a parameter to this error.
168    pub fn with_param(mut self, key: impl Into<String>, value: impl Into<ErrorParam>) -> Self {
169        self.params.insert(key.into(), value.into());
170        self
171    }
172
173    /// Get the field name (last segment of path)
174    pub fn field_name(&self) -> Option<&str> {
175        self.path.last_field_name()
176    }
177}
178
179impl fmt::Display for ValidationError {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        if self.path.is_empty() {
182            write!(f, "{}", self.message)
183        } else {
184            write!(f, "{}: {}", self.path, self.message)
185        }
186    }
187}
188
189impl std::error::Error for ValidationError {}
190
191/// Container for field-level errors, supporting nested structures.
192#[derive(Debug, Clone)]
193#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
194#[cfg_attr(feature = "serde", serde(untagged))]
195pub enum FieldErrors {
196    /// Simple list of errors for this field
197    Simple(Vec<ValidationError>),
198
199    /// Nested struct errors
200    Nested(Box<ValidationErrors>),
201
202    /// List/array item errors (index -> errors)
203    List(BTreeMap<usize, Box<ValidationErrors>>),
204
205    /// Map item errors (key -> errors)
206    Map(BTreeMap<String, Box<ValidationErrors>>),
207}
208
209impl FieldErrors {
210    /// Create simple field errors
211    pub fn simple(errors: Vec<ValidationError>) -> Self {
212        Self::Simple(errors)
213    }
214
215    /// Create nested struct errors
216    pub fn nested(errors: ValidationErrors) -> Self {
217        Self::Nested(Box::new(errors))
218    }
219
220    /// Create list errors
221    pub fn list(errors: BTreeMap<usize, Box<ValidationErrors>>) -> Self {
222        Self::List(errors)
223    }
224
225    /// Create map errors
226    pub fn map(errors: BTreeMap<String, Box<ValidationErrors>>) -> Self {
227        Self::Map(errors)
228    }
229
230    /// Check if empty
231    pub fn is_empty(&self) -> bool {
232        match self {
233            Self::Simple(v) => v.is_empty(),
234            Self::Nested(n) => n.is_empty(),
235            Self::List(m) => m.is_empty(),
236            Self::Map(m) => m.is_empty(),
237        }
238    }
239
240    /// Count total errors recursively
241    pub fn count(&self) -> usize {
242        match self {
243            Self::Simple(v) => v.len(),
244            Self::Nested(n) => n.count(),
245            Self::List(m) => m.values().map(|v| v.count()).sum(),
246            Self::Map(m) => m.values().map(|v| v.count()).sum(),
247        }
248    }
249}
250
251/// Container for validation errors with nested structure support.
252///
253/// # Example
254///
255/// ```rust
256/// use skp_validator_core::{ValidationErrors, ValidationError};
257///
258/// let mut errors = ValidationErrors::new();
259/// errors.add_field_error("email", ValidationError::new("email", "email.invalid", "Invalid email"));
260/// errors.add_field_error("name", ValidationError::new("name", "required", "Name is required"));
261///
262/// assert_eq!(errors.count(), 2);
263/// ```
264#[derive(Debug, Clone, Default)]
265#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
266pub struct ValidationErrors {
267    /// Root-level errors (struct-wide validations)
268    #[cfg_attr(
269        feature = "serde",
270        serde(default, skip_serializing_if = "Vec::is_empty")
271    )]
272    pub errors: Vec<ValidationError>,
273
274    /// Field-level errors
275    #[cfg_attr(
276        feature = "serde",
277        serde(default, skip_serializing_if = "BTreeMap::is_empty")
278    )]
279    pub fields: BTreeMap<String, FieldErrors>,
280}
281
282impl ValidationErrors {
283    /// Create an empty error container
284    pub fn new() -> Self {
285        Self::default()
286    }
287
288    /// Create from a single error
289    pub fn from_error(error: ValidationError) -> Self {
290        let mut errors = Self::new();
291        if let Some(field) = error.field_name().map(|s| s.to_string()) {
292            errors.add_field_error(field, error);
293        } else {
294            errors.add_root_error(error);
295        }
296        errors
297    }
298
299    /// Check if there are no errors
300    pub fn is_empty(&self) -> bool {
301        self.errors.is_empty() && self.fields.is_empty()
302    }
303
304    /// Count total errors recursively
305    pub fn count(&self) -> usize {
306        self.errors.len() + self.fields.values().map(|f| f.count()).sum::<usize>()
307    }
308
309    /// Add a root-level error (not associated with a specific field)
310    pub fn add_root_error(&mut self, error: ValidationError) {
311        self.errors.push(error);
312    }
313
314    /// Add a field-level error
315    pub fn add_field_error(&mut self, field: impl Into<String>, error: ValidationError) {
316        let field = field.into();
317        match self.fields.entry(field) {
318            std::collections::btree_map::Entry::Occupied(mut e) => {
319                if let FieldErrors::Simple(vec) = e.get_mut() {
320                    vec.push(error);
321                }
322            }
323            std::collections::btree_map::Entry::Vacant(e) => {
324                e.insert(FieldErrors::Simple(vec![error]));
325            }
326        }
327    }
328
329    /// Add nested struct errors
330    pub fn add_nested_errors(&mut self, field: impl Into<String>, errors: ValidationErrors) {
331        if !errors.is_empty() {
332            self.fields
333                .insert(field.into(), FieldErrors::Nested(Box::new(errors)));
334        }
335    }
336
337    /// Add list item errors
338    pub fn add_list_errors(
339        &mut self,
340        field: impl Into<String>,
341        errors: BTreeMap<usize, Box<ValidationErrors>>,
342    ) {
343        if !errors.is_empty() {
344            self.fields.insert(field.into(), FieldErrors::List(errors));
345        }
346    }
347
348    /// Add map item errors
349    pub fn add_map_errors(
350        &mut self,
351        field: impl Into<String>,
352        errors: BTreeMap<String, Box<ValidationErrors>>,
353    ) {
354        if !errors.is_empty() {
355            self.fields.insert(field.into(), FieldErrors::Map(errors));
356        }
357    }
358
359    /// Merge another ValidationErrors into this one
360    pub fn merge(&mut self, other: ValidationErrors) {
361        self.errors.extend(other.errors);
362        for (field, errors) in other.fields {
363            self.fields.insert(field, errors);
364        }
365    }
366
367    /// Merge errors from a Result
368    pub fn merge_result<T>(&mut self, result: Result<T, ValidationErrors>) -> Option<T> {
369        match result {
370            Ok(v) => Some(v),
371            Err(e) => {
372                self.merge(e);
373                None
374            }
375        }
376    }
377
378    /// Convert to a flat map format: "path" -> ["error1", "error2"]
379    ///
380    /// Useful for simpler error handling or compatibility with other formats.
381    pub fn to_flat_map(&self) -> BTreeMap<String, Vec<String>> {
382        let mut result = BTreeMap::new();
383        self.flatten_into("", &mut result);
384        result
385    }
386
387    fn flatten_into(&self, prefix: &str, result: &mut BTreeMap<String, Vec<String>>) {
388        // Add root errors
389        for error in &self.errors {
390            let key = if prefix.is_empty() {
391                "_root".to_string()
392            } else {
393                prefix.to_string()
394            };
395            result.entry(key).or_default().push(error.message.clone());
396        }
397
398        // Add field errors
399        for (field, errors) in &self.fields {
400            let path = if prefix.is_empty() {
401                field.clone()
402            } else {
403                format!("{}.{}", prefix, field)
404            };
405
406            match errors {
407                FieldErrors::Simple(vec) => {
408                    for error in vec {
409                        result.entry(path.clone()).or_default().push(error.message.clone());
410                    }
411                }
412                FieldErrors::Nested(nested) => {
413                    nested.flatten_into(&path, result);
414                }
415                FieldErrors::List(list) => {
416                    for (idx, nested) in list {
417                        let item_path = format!("{}[{}]", path, idx);
418                        nested.flatten_into(&item_path, result);
419                    }
420                }
421                FieldErrors::Map(map) => {
422                    for (key, nested) in map {
423                        let item_path = format!("{}[{}]", path, key);
424                        nested.flatten_into(&item_path, result);
425                    }
426                }
427            }
428        }
429    }
430
431    /// Get all error messages as a flat list
432    pub fn messages(&self) -> Vec<String> {
433        let flat = self.to_flat_map();
434        flat.into_values().flatten().collect()
435    }
436
437    /// Get errors for a specific field
438    pub fn field(&self, name: &str) -> Option<&FieldErrors> {
439        self.fields.get(name)
440    }
441
442    /// Check if a specific field has errors
443    pub fn has_field_error(&self, name: &str) -> bool {
444        self.fields.contains_key(name)
445    }
446
447    /// Get the number of fields with errors
448    pub fn field_count(&self) -> usize {
449        self.fields.len()
450    }
451
452    /// Iterate over field errors
453    pub fn field_errors(&self) -> impl Iterator<Item = (&String, &FieldErrors)> {
454        self.fields.iter()
455    }
456
457    /// Convert to JSON value (requires serde feature)
458    #[cfg(feature = "serde")]
459    pub fn to_json(&self) -> serde_json::Value {
460        serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
461    }
462
463    /// Convert to JSON string (requires serde feature)
464    #[cfg(feature = "serde")]
465    pub fn to_json_string(&self) -> String {
466        serde_json::to_string(self).unwrap_or_default()
467    }
468
469    /// Convert to pretty JSON string (requires serde feature)
470    #[cfg(feature = "serde")]
471    pub fn to_json_pretty(&self) -> String {
472        serde_json::to_string_pretty(self).unwrap_or_default()
473    }
474}
475
476impl std::error::Error for ValidationErrors {}
477
478impl fmt::Display for ValidationErrors {
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        let flat = self.to_flat_map();
481        let mut first = true;
482        for (field, messages) in flat {
483            for msg in messages {
484                if !first {
485                    writeln!(f)?;
486                }
487                first = false;
488                if field == "_root" {
489                    write!(f, "{}", msg)?;
490                } else {
491                    write!(f, "{}: {}", field, msg)?;
492                }
493            }
494        }
495        Ok(())
496    }
497}
498
499impl FromIterator<ValidationError> for ValidationErrors {
500    fn from_iter<I: IntoIterator<Item = ValidationError>>(iter: I) -> Self {
501        let mut errors = ValidationErrors::new();
502        for error in iter {
503            if let Some(field) = error.field_name() {
504                errors.add_field_error(field.to_string(), error);
505            } else {
506                errors.add_root_error(error);
507            }
508        }
509        errors
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn test_single_error() {
519        let mut errors = ValidationErrors::new();
520        errors.add_field_error(
521            "email",
522            ValidationError::new("email", "email.invalid", "Invalid email"),
523        );
524
525        assert!(!errors.is_empty());
526        assert_eq!(errors.count(), 1);
527        assert!(errors.has_field_error("email"));
528    }
529
530    #[test]
531    fn test_multiple_errors() {
532        let mut errors = ValidationErrors::new();
533        errors.add_field_error(
534            "email",
535            ValidationError::new("email", "email.invalid", "Invalid email"),
536        );
537        errors.add_field_error(
538            "name",
539            ValidationError::new("name", "required", "Name is required"),
540        );
541
542        assert_eq!(errors.count(), 2);
543    }
544
545    #[test]
546    fn test_flat_map() {
547        let mut errors = ValidationErrors::new();
548        errors.add_field_error(
549            "email",
550            ValidationError::new("email", "email.invalid", "Invalid email"),
551        );
552
553        let flat = errors.to_flat_map();
554        assert!(flat.contains_key("email"));
555        assert_eq!(flat["email"][0], "Invalid email");
556    }
557
558    #[test]
559    fn test_nested_errors() {
560        let mut inner = ValidationErrors::new();
561        inner.add_field_error(
562            "city",
563            ValidationError::new("city", "required", "City is required"),
564        );
565
566        let mut outer = ValidationErrors::new();
567        outer.add_nested_errors("address", inner);
568
569        assert_eq!(outer.count(), 1);
570        let flat = outer.to_flat_map();
571        assert!(flat.contains_key("address.city"));
572    }
573}