Skip to main content

vld_integration_common/
form.rs

1//! Framework-agnostic server-function / form validation shared by
2//! `vld-dioxus` and `vld-leptos`.
3
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7/// A single field validation error.
8///
9/// `field` is a clean dotted path (`name`, `user.email`, `items[0]`).
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11pub struct FieldError {
12    /// Field path (matches the server function argument name or struct field).
13    pub field: String,
14    /// Human-readable error message.
15    pub message: String,
16}
17
18/// Structured, serializable validation error for server functions.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20pub struct VldServerError {
21    /// Summary message.
22    pub message: String,
23    /// Per-field validation errors.
24    pub fields: Vec<FieldError>,
25}
26
27impl VldServerError {
28    /// Create a validation error from a list of field errors.
29    pub fn validation(fields: Vec<FieldError>) -> Self {
30        let count = fields.len();
31        Self {
32            message: format!(
33                "Validation failed: {} field{} invalid",
34                count,
35                if count == 1 { "" } else { "s" }
36            ),
37            fields,
38        }
39    }
40
41    /// Create an internal/serialization error.
42    pub fn internal(msg: impl Into<String>) -> Self {
43        Self {
44            message: msg.into(),
45            fields: Vec::new(),
46        }
47    }
48
49    /// Get the first error message for a specific field.
50    pub fn field_error(&self, field: &str) -> Option<&str> {
51        self.fields
52            .iter()
53            .find(|f| f.field == field)
54            .map(|f| f.message.as_str())
55    }
56
57    /// Get all error messages for a specific field.
58    pub fn field_errors(&self, field: &str) -> Vec<&str> {
59        self.fields
60            .iter()
61            .filter(|f| f.field == field)
62            .map(|f| f.message.as_str())
63            .collect()
64    }
65
66    /// Check if a specific field has errors.
67    pub fn has_field_error(&self, field: &str) -> bool {
68        self.fields.iter().any(|f| f.field == field)
69    }
70
71    /// List all field names that have errors (unique, in first-seen order).
72    pub fn error_fields(&self) -> Vec<&str> {
73        let mut seen = std::collections::HashSet::new();
74        self.fields
75            .iter()
76            .map(|f| f.field.as_str())
77            .filter(|f| seen.insert(*f))
78            .collect()
79    }
80
81    /// Parse a `VldServerError` from a JSON string (e.g. from a `ServerFnError` message).
82    pub fn from_json(s: &str) -> Option<Self> {
83        serde_json::from_str(s).ok()
84    }
85
86    /// Parse a `VldServerError` out of a server-function error message.
87    ///
88    /// Leptos / Dioxus `ServerFnError`'s `Display` prefixes the message
89    /// (`"error running server function: {...}"`), so [`from_json`](Self::from_json)
90    /// on `e.to_string()` never matches; this reads the JSON object from the
91    /// first `{` to the last `}`.
92    ///
93    /// ```
94    /// use vld_integration_common::form::{FieldError, VldServerError};
95    /// let e = VldServerError::validation(vec![FieldError { field: "name".into(), message: "too short".into() }]);
96    /// let shown = format!("error running server function: {e}");
97    /// let back = VldServerError::from_server_message(&shown).unwrap();
98    /// assert_eq!(back.field_error("name"), Some("too short"));
99    /// ```
100    pub fn from_server_message(s: &str) -> Option<Self> {
101        let (start, end) = (s.find('{')?, s.rfind('}')?);
102        Self::from_json(s.get(start..=end)?)
103    }
104}
105
106impl fmt::Display for VldServerError {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match serde_json::to_string(self) {
109            Ok(json) => write!(f, "{}", json),
110            Err(_) => write!(f, "{}", self.message),
111        }
112    }
113}
114
115impl std::error::Error for VldServerError {}
116
117impl From<vld::error::VldError> for VldServerError {
118    fn from(error: vld::error::VldError) -> Self {
119        Self::validation(field_errors(&error))
120    }
121}
122
123fn field_errors(error: &vld::error::VldError) -> Vec<FieldError> {
124    crate::flatten_issues(error)
125        .into_iter()
126        .map(|(field, message)| FieldError { field, message })
127        .collect()
128}
129
130fn ser_error(e: serde_json::Error) -> String {
131    format!("Serialization error: {}", e)
132}
133
134/// Validate a serializable value against a vld schema type.
135pub fn validate<S, T>(data: &T) -> Result<(), VldServerError>
136where
137    S: vld::schema::VldParse,
138    T: serde::Serialize,
139{
140    let json = serde_json::to_value(data).map_err(|e| VldServerError::internal(ser_error(e)))?;
141    validate_value::<S>(&json)
142}
143
144/// Validate a `serde_json::Value` against a vld schema type.
145pub fn validate_value<S>(json: &serde_json::Value) -> Result<(), VldServerError>
146where
147    S: vld::schema::VldParse,
148{
149    S::vld_parse_value(json).map_err(VldServerError::from)?;
150    Ok(())
151}
152
153/// Check a single value against a schema, returning the first error message if invalid.
154///
155/// A value that cannot be serialized is reported as an error, never as valid.
156pub fn check_field<V, S>(value: &V, schema: &S) -> Option<String>
157where
158    V: serde::Serialize,
159    S: vld::schema::VldSchema,
160{
161    check_field_all(value, schema).into_iter().next()
162}
163
164/// Check a single value, returning all error messages (empty if valid).
165pub fn check_field_all<V, S>(value: &V, schema: &S) -> Vec<String>
166where
167    V: serde::Serialize,
168    S: vld::schema::VldSchema,
169{
170    let json = match serde_json::to_value(value) {
171        Ok(j) => j,
172        Err(e) => return vec![ser_error(e)],
173    };
174    match schema.parse_value(&json) {
175        Ok(_) => vec![],
176        Err(e) => e.issues.iter().map(|i| i.message.clone()).collect(),
177    }
178}
179
180/// Validate all fields of a serializable struct against a vld schema type.
181///
182/// Returns a list of [`FieldError`]s (empty if valid). A serialization failure
183/// is reported as a single error with an empty `field`.
184pub fn check_all_fields<S, T>(data: &T) -> Vec<FieldError>
185where
186    S: vld::schema::VldParse,
187    T: serde::Serialize,
188{
189    let json = match serde_json::to_value(data) {
190        Ok(j) => j,
191        Err(e) => {
192            return vec![FieldError {
193                field: String::new(),
194                message: ser_error(e),
195            }];
196        }
197    };
198    match S::vld_parse_value(&json) {
199        Ok(_) => vec![],
200        Err(e) => field_errors(&e),
201    }
202}
203
204#[doc(hidden)]
205pub fn __arg_errors<V, S>(name: &str, value: &V, schema: &S, out: &mut Vec<FieldError>)
206where
207    V: serde::Serialize,
208    S: vld::schema::VldSchema,
209{
210    let json = match serde_json::to_value(value) {
211        Ok(j) => j,
212        Err(e) => {
213            out.push(FieldError {
214                field: name.to_string(),
215                message: ser_error(e),
216            });
217            return;
218        }
219    };
220    if let Err(e) = schema.parse_value(&json) {
221        for issue in &e.issues {
222            let sub = crate::format_path(&issue.path);
223            let field = if sub.is_empty() {
224                name.to_string()
225            } else if sub.starts_with('[') {
226                format!("{name}{sub}")
227            } else {
228                format!("{name}.{sub}")
229            };
230            out.push(FieldError {
231                field,
232                message: issue.message.clone(),
233            });
234        }
235    }
236}
237
238/// Validate function arguments inline, accumulating all errors into a
239/// `VldServerError`. Field names are the argument names (nested issues get
240/// `arg.sub` paths).
241#[macro_export]
242macro_rules! validate_args {
243    ($($field:ident => $schema:expr),* $(,)?) => {{
244        let mut __vld_field_errors = ::std::vec::Vec::new();
245        $(
246            $crate::form::__arg_errors(stringify!($field), &$field, &($schema), &mut __vld_field_errors);
247        )*
248        if __vld_field_errors.is_empty() {
249            ::std::result::Result::Ok::<(), $crate::form::VldServerError>(())
250        } else {
251            ::std::result::Result::Err($crate::form::VldServerError::validation(__vld_field_errors))
252        }
253    }};
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    vld::schema! {
261        #[derive(Debug)]
262        struct Inner { email: String => vld::string().email() }
263    }
264    vld::schema! {
265        #[derive(Debug)]
266        struct Outer {
267            name: String => vld::string().min(2),
268            user: Inner => vld::nested(Inner::parse_value),
269        }
270    }
271
272    #[test]
273    fn paths_match_validate_args() {
274        let err: VldServerError =
275            Outer::parse_value(&serde_json::json!({"name": "A", "user": {"email": "bad"}}))
276                .unwrap_err()
277                .into();
278        assert!(err.field_error("name").is_some());
279        assert!(err.field_error("user.email").is_some());
280
281        let name = "A".to_string();
282        let err = crate::validate_args! { name => vld::string().min(2) }.unwrap_err();
283        assert!(err.field_error("name").is_some());
284    }
285
286    #[test]
287    fn error_fields_dedup_non_adjacent() {
288        let f = |n: &str| FieldError {
289            field: n.into(),
290            message: "x".into(),
291        };
292        let e = VldServerError::validation(vec![f("a"), f("b"), f("a")]);
293        assert_eq!(e.error_fields(), vec!["a", "b"]);
294    }
295
296    struct Bad;
297    impl serde::Serialize for Bad {
298        fn serialize<S: serde::Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
299            Err(serde::ser::Error::custom("nope"))
300        }
301    }
302
303    #[test]
304    fn serialization_failure_is_an_error() {
305        assert!(check_field(&Bad, &vld::string()).is_some());
306        assert!(!check_field_all(&Bad, &vld::string()).is_empty());
307        assert!(!check_all_fields::<Inner, _>(&Bad).is_empty());
308    }
309}