Skip to main content

vld_dioxus/
lib.rs

1//! # vld-dioxus — Dioxus integration for the `vld` validation library
2//!
3//! Shared validation for Dioxus server functions and WASM clients.
4//! Define validation rules once, use them on both the server and the browser.
5//!
6//! **Zero dependency on `dioxus`** — works with any Dioxus version (0.5, 0.6, 0.7+).
7//! Compatible with WASM and native targets.
8//!
9//! ## Key Features
10//!
11//! | Feature | Description |
12//! |---|---|
13//! | [`validate_args!`] | Inline validation of server function arguments |
14//! | [`validate`] / [`validate_value`] | Validate a serializable value against a schema |
15//! | [`check_field`] | Single-field validation for reactive UI |
16//! | [`check_all_fields`] | Multi-field validation returning per-field errors |
17//! | [`VldServerError`] | Serializable error type for server→client error transport |
18//!
19//! ## Quick Example
20//!
21//! ```ignore
22//! // Shared validation rules (used on server AND client)
23//! fn name_schema() -> vld::primitives::ZString { vld::string().min(2).max(50) }
24//! fn email_schema() -> vld::primitives::ZString { vld::string().email() }
25//!
26//! // Server function
27//! #[server]
28//! async fn create_user(name: String, email: String) -> Result<(), ServerFnError> {
29//!     vld_dioxus::validate_args! {
30//!         name  => name_schema(),
31//!         email => email_schema(),
32//!     }.map_err(|e| ServerFnError::new(e.to_string()))?;
33//!     Ok(())
34//! }
35//!
36//! // Client component (reactive validation)
37//! #[component]
38//! fn CreateUserForm() -> Element {
39//!     let mut name = use_signal(String::new);
40//!     let name_err = use_memo(move || {
41//!         vld_dioxus::check_field(&name(), &name_schema())
42//!     });
43//!     // ... render form with error display
44//! }
45//! ```
46
47use serde::{Deserialize, Serialize};
48use std::fmt;
49
50// ========================= Error types =======================================
51
52/// A single field validation error.
53///
54/// Designed to be serialized across the server→client boundary
55/// and displayed in form UIs.
56#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct FieldError {
58    /// Field name (matches the server function argument name or struct field).
59    pub field: String,
60    /// Human-readable error message.
61    pub message: String,
62}
63
64/// Structured validation error for Dioxus server functions.
65///
66/// Serializable and deserializable — can be transmitted from server to client
67/// as part of a `ServerFnError` message or custom error type.
68///
69/// # With `ServerFnError`
70///
71/// ```ignore
72/// #[server]
73/// async fn my_fn(name: String) -> Result<(), ServerFnError> {
74///     vld_dioxus::validate_args! {
75///         name => vld::string().min(2),
76///     }.map_err(|e| ServerFnError::new(e.to_string()))?;
77///     Ok(())
78/// }
79/// ```
80///
81/// # With custom error type
82///
83/// ```ignore
84/// #[derive(Debug, Clone, Serialize, Deserialize)]
85/// enum AppError {
86///     Validation(VldServerError),
87///     Server(String),
88/// }
89///
90/// impl FromServerFnError for AppError { /* ... */ }
91///
92/// #[server]
93/// async fn my_fn(name: String) -> Result<(), AppError> {
94///     vld_dioxus::validate_args! {
95///         name => vld::string().min(2),
96///     }.map_err(AppError::Validation)?;
97///     Ok(())
98/// }
99/// ```
100#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
101pub struct VldServerError {
102    /// Summary message.
103    pub message: String,
104    /// Per-field validation errors.
105    pub fields: Vec<FieldError>,
106}
107
108impl VldServerError {
109    /// Create a validation error from a list of field errors.
110    pub fn validation(fields: Vec<FieldError>) -> Self {
111        let count = fields.len();
112        Self {
113            message: format!(
114                "Validation failed: {} field{} invalid",
115                count,
116                if count == 1 { "" } else { "s" }
117            ),
118            fields,
119        }
120    }
121
122    /// Create an internal/serialization error.
123    pub fn internal(msg: impl Into<String>) -> Self {
124        Self {
125            message: msg.into(),
126            fields: Vec::new(),
127        }
128    }
129
130    /// Get the error message for a specific field.
131    pub fn field_error(&self, field: &str) -> Option<&str> {
132        self.fields
133            .iter()
134            .find(|f| f.field == field)
135            .map(|f| f.message.as_str())
136    }
137
138    /// Get all error messages for a specific field.
139    pub fn field_errors(&self, field: &str) -> Vec<&str> {
140        self.fields
141            .iter()
142            .filter(|f| f.field == field)
143            .map(|f| f.message.as_str())
144            .collect()
145    }
146
147    /// Check if a specific field has errors.
148    pub fn has_field_error(&self, field: &str) -> bool {
149        self.fields.iter().any(|f| f.field == field)
150    }
151
152    /// List all field names that have errors.
153    pub fn error_fields(&self) -> Vec<&str> {
154        let mut names: Vec<&str> = self.fields.iter().map(|f| f.field.as_str()).collect();
155        names.dedup();
156        names
157    }
158
159    /// Parse a `VldServerError` from a JSON string (e.g. from `ServerFnError` message).
160    ///
161    /// Returns `None` if the string is not a valid `VldServerError` JSON.
162    pub fn from_json(s: &str) -> Option<Self> {
163        serde_json::from_str(s).ok()
164    }
165}
166
167impl fmt::Display for VldServerError {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        match serde_json::to_string(self) {
170            Ok(json) => write!(f, "{}", json),
171            Err(_) => write!(f, "{}", self.message),
172        }
173    }
174}
175
176impl std::error::Error for VldServerError {}
177
178impl From<vld::error::VldError> for VldServerError {
179    fn from(error: vld::error::VldError) -> Self {
180        let fields: Vec<FieldError> = error
181            .issues
182            .iter()
183            .map(|issue| {
184                let field = issue
185                    .path
186                    .iter()
187                    .map(|p| p.to_string())
188                    .collect::<Vec<_>>()
189                    .join(".");
190                FieldError {
191                    field,
192                    message: issue.message.clone(),
193                }
194            })
195            .collect();
196        Self::validation(fields)
197    }
198}
199
200// ========================= Schema-based validation ===========================
201
202/// Validate a serializable value against a vld schema type.
203///
204/// Serializes `data` to JSON, then validates using `S::vld_parse_value()`.
205///
206/// # Example
207///
208/// ```ignore
209/// vld::schema! {
210///     struct UserInput {
211///         name: String => vld::string().min(2),
212///         email: String => vld::string().email(),
213///     }
214/// }
215///
216/// #[derive(Serialize)]
217/// struct Args { name: String, email: String }
218///
219/// vld_dioxus::validate::<UserInput, _>(&Args { name, email })?;
220/// ```
221pub fn validate<S, T>(data: &T) -> Result<(), VldServerError>
222where
223    S: vld::schema::VldParse,
224    T: serde::Serialize,
225{
226    let json = serde_json::to_value(data)
227        .map_err(|e| VldServerError::internal(format!("Serialization error: {}", e)))?;
228    S::vld_parse_value(&json).map_err(VldServerError::from)?;
229    Ok(())
230}
231
232/// Validate a `serde_json::Value` against a vld schema type.
233pub fn validate_value<S>(json: &serde_json::Value) -> Result<(), VldServerError>
234where
235    S: vld::schema::VldParse,
236{
237    S::vld_parse_value(json).map_err(VldServerError::from)?;
238    Ok(())
239}
240
241// ========================= Field-level validation ============================
242
243/// Check a single value against a schema, returning an error message if invalid.
244///
245/// Designed for reactive client-side validation in Dioxus components.
246/// Returns `None` if valid, `Some(message)` if invalid.
247///
248/// # Example
249///
250/// ```
251/// let error = vld_dioxus::check_field(&"A".to_string(), &vld::string().min(2));
252/// assert!(error.is_some());
253///
254/// let error = vld_dioxus::check_field(&"Alice".to_string(), &vld::string().min(2));
255/// assert!(error.is_none());
256/// ```
257pub fn check_field<V, S>(value: &V, schema: &S) -> Option<String>
258where
259    V: serde::Serialize,
260    S: vld::schema::VldSchema,
261{
262    let json = serde_json::to_value(value).ok()?;
263    match schema.parse_value(&json) {
264        Ok(_) => None,
265        Err(e) => e.issues.first().map(|i| i.message.clone()),
266    }
267}
268
269/// Check a single value, returning all error messages (not just the first).
270///
271/// # Example
272///
273/// ```
274/// let errors = vld_dioxus::check_field_all(&"".to_string(), &vld::string().min(2).email());
275/// assert!(errors.len() >= 1);
276/// ```
277pub fn check_field_all<V, S>(value: &V, schema: &S) -> Vec<String>
278where
279    V: serde::Serialize,
280    S: vld::schema::VldSchema,
281{
282    let json = match serde_json::to_value(value) {
283        Ok(j) => j,
284        Err(_) => return vec![],
285    };
286    match schema.parse_value(&json) {
287        Ok(_) => vec![],
288        Err(e) => e.issues.iter().map(|i| i.message.clone()).collect(),
289    }
290}
291
292/// Validate all fields of a serializable struct against a vld schema type.
293///
294/// Returns a list of [`FieldError`]s (empty if all fields are valid).
295/// Designed for validating entire form state at once.
296///
297/// # Example
298///
299/// ```
300/// use vld_dioxus::FieldError;
301///
302/// vld::schema! {
303///     struct UserSchema {
304///         name: String => vld::string().min(2),
305///         email: String => vld::string().email(),
306///     }
307/// }
308///
309/// #[derive(serde::Serialize)]
310/// struct FormData { name: String, email: String }
311///
312/// let data = FormData { name: "A".into(), email: "bad".into() };
313/// let errors = vld_dioxus::check_all_fields::<UserSchema, _>(&data);
314/// assert!(!errors.is_empty());
315/// assert!(errors.iter().any(|e| e.field == ".name"));
316/// ```
317pub fn check_all_fields<S, T>(data: &T) -> Vec<FieldError>
318where
319    S: vld::schema::VldParse,
320    T: serde::Serialize,
321{
322    let json = match serde_json::to_value(data) {
323        Ok(j) => j,
324        Err(_) => return vec![],
325    };
326    match S::vld_parse_value(&json) {
327        Ok(_) => vec![],
328        Err(e) => e
329            .issues
330            .iter()
331            .map(|i| FieldError {
332                field: i
333                    .path
334                    .iter()
335                    .map(|p| p.to_string())
336                    .collect::<Vec<_>>()
337                    .join("."),
338                message: i.message.clone(),
339            })
340            .collect(),
341    }
342}
343
344// ========================= Macro =============================================
345
346/// Validate server function arguments inline.
347///
348/// Each argument is validated against its schema. All errors are accumulated
349/// and returned as a [`VldServerError`].
350///
351/// # Example
352///
353/// ```ignore
354/// #[server]
355/// async fn create_user(name: String, email: String, age: i64) -> Result<(), ServerFnError> {
356///     vld_dioxus::validate_args! {
357///         name  => vld::string().min(2).max(50),
358///         email => vld::string().email(),
359///         age   => vld::number().int().min(0).max(150),
360///     }.map_err(|e| ServerFnError::new(e.to_string()))?;
361///
362///     // ... all arguments are valid
363///     Ok(())
364/// }
365/// ```
366///
367/// # Shared Schemas
368///
369/// Define schema factories to share between server and client:
370///
371/// ```ignore
372/// // shared.rs — compiles for both server and WASM
373/// pub fn name_schema() -> impl vld::schema::VldSchema<Output = String> {
374///     vld::string().min(2).max(50)
375/// }
376///
377/// // server function
378/// vld_dioxus::validate_args! {
379///     name => shared::name_schema(),
380/// }.map_err(|e| ServerFnError::new(e.to_string()))?;
381///
382/// // client component (use_memo)
383/// let err = use_memo(move || vld_dioxus::check_field(&name(), &shared::name_schema()));
384/// ```
385#[macro_export]
386macro_rules! validate_args {
387    ($($field:ident => $schema:expr),* $(,)?) => {{
388        use ::vld::schema::VldSchema as _;
389        let mut __vld_field_errors: ::std::vec::Vec<$crate::FieldError> = ::std::vec::Vec::new();
390
391        $(
392            {
393                let __vld_json = ::vld::serde_json::to_value(&$field)
394                    .unwrap_or(::vld::serde_json::Value::Null);
395                if let ::std::result::Result::Err(e) = ($schema).parse_value(&__vld_json) {
396                    for issue in &e.issues {
397                        __vld_field_errors.push($crate::FieldError {
398                            field: ::std::string::String::from(stringify!($field)),
399                            message: issue.message.clone(),
400                        });
401                    }
402                }
403            }
404        )*
405
406        if __vld_field_errors.is_empty() {
407            ::std::result::Result::Ok::<(), $crate::VldServerError>(())
408        } else {
409            ::std::result::Result::Err($crate::VldServerError::validation(__vld_field_errors))
410        }
411    }};
412}
413
414/// Prelude — import everything you need.
415pub mod prelude {
416    pub use crate::{
417        check_all_fields, check_field, check_field_all, validate, validate_args, validate_value,
418        FieldError, VldServerError,
419    };
420    pub use vld::prelude::*;
421}