Skip to main content

vld_leptos/
lib.rs

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