Skip to main content

vld/
format.rs

1use std::collections::HashMap;
2
3use crate::error::VldError;
4
5/// Flat error structure, useful for form validation.
6///
7/// - `form_errors`: top-level errors (no path)
8/// - `field_errors`: errors grouped by top-level field name
9#[derive(Debug, Clone)]
10#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
11#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
12pub struct FlatError {
13    pub form_errors: Vec<String>,
14    pub field_errors: HashMap<String, Vec<String>>,
15}
16
17/// Flatten a `VldError` into a simple field-based structure.
18///
19/// # Example
20/// ```
21/// use vld::format::flatten_error;
22/// use vld::error::VldError;
23///
24/// let err = VldError::new(); // empty for demo
25/// let flat = flatten_error(&err);
26/// assert!(flat.form_errors.is_empty());
27/// ```
28pub fn flatten_error(error: &VldError) -> FlatError {
29    let mut form_errors = Vec::new();
30    let mut field_errors: HashMap<String, Vec<String>> = HashMap::new();
31
32    for issue in &error.issues {
33        if issue.path.is_empty() {
34            form_errors.push(issue.message.clone());
35        } else {
36            let key = issue.path[0].to_string();
37            // Strip leading dot from field names
38            let key = key.strip_prefix('.').unwrap_or(&key).to_string();
39            field_errors
40                .entry(key)
41                .or_default()
42                .push(issue.message.clone());
43        }
44    }
45
46    FlatError {
47        form_errors,
48        field_errors,
49    }
50}
51
52/// Tree-based error structure, mirrors the schema shape.
53#[derive(Debug, Clone, Default)]
54#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
55#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
56pub struct ErrorTree {
57    pub errors: Vec<String>,
58    pub properties: HashMap<String, ErrorTree>,
59    pub items: Vec<Option<ErrorTree>>,
60}
61
62/// Convert a `VldError` into a tree structure that mirrors the schema.
63pub fn treeify_error(error: &VldError) -> ErrorTree {
64    let mut root = ErrorTree::default();
65
66    for issue in &error.issues {
67        if issue.path.is_empty() {
68            root.errors.push(issue.message.clone());
69            continue;
70        }
71
72        let mut current = &mut root;
73
74        for (i, segment) in issue.path.iter().enumerate() {
75            let is_last = i == issue.path.len() - 1;
76
77            match segment {
78                crate::error::PathSegment::Field(name) => {
79                    if !current.properties.contains_key(name) {
80                        current
81                            .properties
82                            .insert(name.clone(), ErrorTree::default());
83                    }
84                    if is_last {
85                        current
86                            .properties
87                            .get_mut(name)
88                            .unwrap()
89                            .errors
90                            .push(issue.message.clone());
91                    } else {
92                        current = current.properties.get_mut(name).unwrap();
93                    }
94                }
95                crate::error::PathSegment::Index(idx) => {
96                    while current.items.len() <= *idx {
97                        current.items.push(None);
98                    }
99                    if current.items[*idx].is_none() {
100                        current.items[*idx] = Some(ErrorTree::default());
101                    }
102                    if is_last {
103                        current.items[*idx]
104                            .as_mut()
105                            .unwrap()
106                            .errors
107                            .push(issue.message.clone());
108                    } else {
109                        current = current.items[*idx].as_mut().unwrap();
110                    }
111                }
112            }
113        }
114    }
115
116    root
117}
118
119/// Format a `VldError` into a human-readable string.
120///
121/// # Example output
122/// ```text
123/// ✖ String must be at least 2 characters
124///   → at .name
125/// ✖ Invalid email address
126///   → at .email
127/// ```
128pub fn prettify_error(error: &VldError) -> String {
129    let mut lines = Vec::new();
130
131    for issue in &error.issues {
132        lines.push(format!("✖ {}", issue.message));
133        if !issue.path.is_empty() || issue.received.is_some() {
134            let mut parts = Vec::new();
135            if !issue.path.is_empty() {
136                let path_str: String = issue.path.iter().map(|p| p.to_string()).collect();
137                parts.push(format!("at {}", path_str));
138            }
139            if let Some(val) = &issue.received {
140                parts.push(format!(
141                    "received {}",
142                    crate::error::format_value_short(val)
143                ));
144            }
145            lines.push(format!("  → {}", parts.join(", ")));
146        }
147    }
148
149    lines.join("\n")
150}