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}