Skip to main content

vld/error/
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, serde::Serialize, serde::Deserialize)]
10pub struct FlatError {
11    /// Messages of issues at the root (empty path).
12    pub form_errors: Vec<String>,
13    /// Messages per top-level field.
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, serde::Serialize, serde::Deserialize)]
54pub struct ErrorTree {
55    /// Messages of issues at this node.
56    pub errors: Vec<String>,
57    /// Child nodes per object key.
58    pub properties: HashMap<String, ErrorTree>,
59    /// Child nodes per array index (`None` where an item has no issues).
60    pub items: Vec<Option<ErrorTree>>,
61}
62
63/// Convert a `VldError` into a tree structure that mirrors the schema.
64pub fn treeify_error(error: &VldError) -> ErrorTree {
65    let mut root = ErrorTree::default();
66
67    for issue in &error.issues {
68        if issue.path.is_empty() {
69            root.errors.push(issue.message.clone());
70            continue;
71        }
72
73        let mut current = &mut root;
74
75        for (i, segment) in issue.path.iter().enumerate() {
76            let is_last = i == issue.path.len() - 1;
77
78            match segment {
79                crate::error::PathSegment::Field(name) => {
80                    if !current.properties.contains_key(name) {
81                        current
82                            .properties
83                            .insert(name.clone(), ErrorTree::default());
84                    }
85                    if is_last {
86                        current
87                            .properties
88                            .get_mut(name)
89                            .unwrap()
90                            .errors
91                            .push(issue.message.clone());
92                    } else {
93                        current = current.properties.get_mut(name).unwrap();
94                    }
95                }
96                crate::error::PathSegment::Index(idx) => {
97                    while current.items.len() <= *idx {
98                        current.items.push(None);
99                    }
100                    if current.items[*idx].is_none() {
101                        current.items[*idx] = Some(ErrorTree::default());
102                    }
103                    if is_last {
104                        current.items[*idx]
105                            .as_mut()
106                            .unwrap()
107                            .errors
108                            .push(issue.message.clone());
109                    } else {
110                        current = current.items[*idx].as_mut().unwrap();
111                    }
112                }
113            }
114        }
115    }
116
117    root
118}
119
120/// Format a `VldError` into a human-readable string.
121///
122/// # Example output
123/// ```text
124/// ✖ String must be at least 2 characters
125///   → at .name
126/// ✖ Invalid email address
127///   → at .email
128/// ```
129pub fn prettify_error(error: &VldError) -> String {
130    let mut lines = Vec::new();
131
132    for issue in &error.issues {
133        lines.push(format!("✖ {}", issue.message));
134        if !issue.path.is_empty() || issue.received.is_some() {
135            let mut parts = Vec::new();
136            if !issue.path.is_empty() {
137                let path_str: String = issue.path.iter().map(|p| p.to_string()).collect();
138                parts.push(format!("at {}", path_str));
139            }
140            if let Some(val) = &issue.received {
141                parts.push(format!(
142                    "received {}",
143                    crate::error::format_value_short(val)
144                ));
145            }
146            lines.push(format!("  → {}", parts.join(", ")));
147        }
148    }
149
150    lines.join("\n")
151}