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}