Skip to main content

vld_tauri/
lib.rs

1//! # vld-tauri — Tauri validation for `vld`
2//!
3//! Provides helpers for validating [Tauri](https://tauri.app/) IPC commands,
4//! events, state, plugin config and channel messages using `vld` schemas.
5//!
6//! **This crate does not depend on `tauri` itself** — it only needs `vld`,
7//! `serde` and `serde_json`, keeping the dependency tree minimal.
8//! You add `tauri` as a separate dependency in your app.
9//!
10//! # Overview
11//!
12//! | Item | Use case |
13//! |------|----------|
14//! | [`validate`] | General-purpose JSON validation |
15//! | [`validate_args`] | Validate a raw JSON string |
16//! | [`validate_event`] | Validate incoming event payloads |
17//! | [`validate_state`] | Validate app state / config at init |
18//! | [`validate_channel_message`] | Validate outgoing channel messages |
19//! | [`validate_plugin_config`] | Validate plugin JSON config |
20//! | [`VldTauriError`] | Serializable error for `#[tauri::command]` results |
21//! | [`VldPayload<T>`] | Auto-validating `Deserialize` wrapper |
22//! | [`VldEvent<T>`] | Auto-validating `Deserialize` wrapper for events |
23//!
24//! # Usage patterns
25//!
26//! ## IPC commands — explicit validation (recommended)
27//!
28//! Accept `serde_json::Value` and validate manually.
29//! The frontend receives structured error JSON on failure.
30//!
31//! ```rust,ignore
32//! use vld_tauri::prelude::*;
33//!
34//! vld::schema! {
35//!     #[derive(Debug, Clone, serde::Serialize)]
36//!     pub struct CreateUser {
37//!         pub name: String  => vld::string().min(2).max(50),
38//!         pub email: String => vld::string().email(),
39//!     }
40//! }
41//!
42//! #[tauri::command]
43//! fn create_user(payload: serde_json::Value) -> Result<String, VldTauriError> {
44//!     let user = validate::<CreateUser>(payload)?;
45//!     Ok(format!("Created {}", user.name))
46//! }
47//! ```
48//!
49//! ## IPC commands — auto-validated payload
50//!
51//! `VldPayload<T>` implements `Deserialize` and validates during
52//! deserialization. If validation fails, Tauri reports a deserialization
53//! error with the full validation message.
54//!
55//! ```rust,ignore
56//! #[tauri::command]
57//! fn create_user(payload: VldPayload<CreateUser>) -> Result<String, VldTauriError> {
58//!     Ok(format!("Created {}", payload.name))
59//! }
60//! ```
61//!
62//! ## Event payloads
63//!
64//! Validate data from `emit()`/`listen()`:
65//!
66//! ```rust,ignore
67//! use tauri::{Emitter, Listener};
68//!
69//! // Backend: validate incoming event from frontend
70//! app.listen("user:update", |event| {
71//!     let payload: serde_json::Value = serde_json::from_str(event.payload()).unwrap();
72//!     match validate_event::<UserUpdate>(payload) {
73//!         Ok(update) => println!("Valid update: {:?}", update),
74//!         Err(e)     => eprintln!("Bad event: {e}"),
75//!     }
76//! });
77//!
78//! // Or auto-validate with VldEvent<T>:
79//! let event: VldEvent<UserUpdate> = serde_json::from_str(event.payload()).unwrap();
80//! ```
81//!
82//! ## State validation at init
83//!
84//! ```rust,ignore
85//! let config_json = std::fs::read_to_string("config.json").unwrap();
86//! let config = validate_state::<AppConfig>(
87//!     serde_json::from_str(&config_json).unwrap()
88//! ).expect("Invalid app config");
89//! app.manage(config);
90//! ```
91//!
92//! ## Plugin config validation
93//!
94//! ```rust,ignore
95//! let plugin_cfg: serde_json::Value = /* from tauri.conf.json */;
96//! let cfg = validate_plugin_config::<MyPluginConfig>(plugin_cfg)
97//!     .expect("Invalid plugin config");
98//! ```
99//!
100//! ## Channel message validation
101//!
102//! ```rust,ignore
103//! #[tauri::command]
104//! fn stream(channel: Channel<serde_json::Value>) -> Result<(), VldTauriError> {
105//!     let msg = ProgressUpdate { percent: 50, status: "working".into() };
106//!     let validated = validate_channel_message::<ProgressUpdate>(
107//!         serde_json::to_value(&msg).unwrap()
108//!     )?;
109//!     channel.send(serde_json::to_value(&validated).unwrap()).unwrap();
110//!     Ok(())
111//! }
112//! ```
113//!
114//! # Frontend usage (TypeScript)
115//!
116//! ```javascript,ignore
117//! import { invoke } from '@tauri-apps/api/core';
118//!
119//! interface VldError {
120//!   error: string;
121//!   issues: Array<{ path: string; message: string }>;
122//! }
123//!
124//! try {
125//!   const result = await invoke('create_user', {
126//!     payload: { name: 'Alice', email: 'alice@example.com' }
127//!   });
128//! } catch (err) {
129//!   const vldErr = err as VldError;
130//!   for (const issue of vldErr.issues) {
131//!     console.error(`${issue.path}: ${issue.message}`);
132//!   }
133//! }
134//! ```
135
136use serde::{Deserialize, Serialize};
137use vld::schema::VldParse;
138
139// ---------------------------------------------------------------------------
140// Error type
141// ---------------------------------------------------------------------------
142
143/// Single validation issue, serialised for the frontend.
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145pub struct TauriIssue {
146    /// JSON-path of the failing field, e.g. `.name`.
147    pub path: String,
148    /// Human-readable error message.
149    pub message: String,
150}
151
152/// Serializable error type for Tauri IPC commands, events, and channel
153/// messages.
154///
155/// Tauri requires command error types to implement `Serialize`.
156/// `VldTauriError` serialises as:
157///
158/// ```json
159/// {
160///   "error": "Validation failed",
161///   "issues": [
162///     { "path": ".name", "message": "String must be at least 2 characters" }
163///   ]
164/// }
165/// ```
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct VldTauriError {
168    /// Error category string (e.g. `"Validation failed"`, `"Invalid JSON"`).
169    pub error: String,
170    /// List of individual validation issues.
171    pub issues: Vec<TauriIssue>,
172}
173
174impl VldTauriError {
175    /// Create from a [`VldError`](vld::error::VldError).
176    pub fn from_vld(e: &vld::error::VldError) -> Self {
177        let issues = e
178            .issues
179            .iter()
180            .map(|issue| {
181                let path: String = issue
182                    .path
183                    .iter()
184                    .map(|p| p.to_string())
185                    .collect::<Vec<_>>()
186                    .join(".");
187                TauriIssue {
188                    path,
189                    message: issue.message.clone(),
190                }
191            })
192            .collect();
193        Self {
194            error: "Validation failed".into(),
195            issues,
196        }
197    }
198
199    /// Build a JSON-parse error.
200    pub fn json_parse_error(msg: impl std::fmt::Display) -> Self {
201        Self {
202            error: "Invalid JSON".into(),
203            issues: vec![TauriIssue {
204                path: String::new(),
205                message: msg.to_string(),
206            }],
207        }
208    }
209
210    /// Build a generic error with a custom category.
211    pub fn custom(error: impl Into<String>, message: impl Into<String>) -> Self {
212        Self {
213            error: error.into(),
214            issues: vec![TauriIssue {
215                path: String::new(),
216                message: message.into(),
217            }],
218        }
219    }
220
221    /// `true` if the error contains any issues.
222    pub fn has_issues(&self) -> bool {
223        !self.issues.is_empty()
224    }
225
226    /// Number of issues.
227    pub fn issue_count(&self) -> usize {
228        self.issues.len()
229    }
230}
231
232impl std::fmt::Display for VldTauriError {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        write!(f, "{}: {} issue(s)", self.error, self.issues.len())
235    }
236}
237
238impl std::error::Error for VldTauriError {}
239
240impl From<vld::error::VldError> for VldTauriError {
241    fn from(e: vld::error::VldError) -> Self {
242        Self::from_vld(&e)
243    }
244}
245
246// ---------------------------------------------------------------------------
247// Core validate function
248// ---------------------------------------------------------------------------
249
250/// Validate a `serde_json::Value` against a `vld` schema.
251///
252/// General-purpose entry-point used by all specialised helpers.
253///
254/// # Example
255///
256/// ```rust
257/// use vld_tauri::validate;
258///
259/// vld::schema! {
260///     #[derive(Debug)]
261///     struct Greet {
262///         name: String => vld::string().min(1),
263///     }
264/// }
265///
266/// let val = serde_json::json!({"name": "Alice"});
267/// let g = validate::<Greet>(val).unwrap();
268/// assert_eq!(g.name, "Alice");
269/// ```
270pub fn validate<T: VldParse>(value: serde_json::Value) -> Result<T, VldTauriError> {
271    T::vld_parse_value(&value).map_err(VldTauriError::from)
272}
273
274/// Validate a raw JSON string against a `vld` schema.
275///
276/// Convenience wrapper — parses the string first, then validates.
277///
278/// # Example
279///
280/// ```rust
281/// use vld_tauri::validate_args;
282///
283/// vld::schema! {
284///     #[derive(Debug)]
285///     struct Ping {
286///         msg: String => vld::string().min(1),
287///     }
288/// }
289///
290/// let p = validate_args::<Ping>(r#"{"msg":"pong"}"#).unwrap();
291/// assert_eq!(p.msg, "pong");
292/// ```
293pub fn validate_args<T: VldParse>(json_str: &str) -> Result<T, VldTauriError> {
294    let value: serde_json::Value =
295        serde_json::from_str(json_str).map_err(VldTauriError::json_parse_error)?;
296    validate(value)
297}
298
299// ---------------------------------------------------------------------------
300// Specialised validators (semantically clear aliases)
301// ---------------------------------------------------------------------------
302
303/// Validate an **event payload** received from the frontend via
304/// `Emitter::emit()` / `Listener::listen()`.
305///
306/// Functionally identical to [`validate`], but conveys intent.
307///
308/// # Example
309///
310/// ```rust,ignore
311/// app.listen("user:update", |event| {
312///     let payload: serde_json::Value = serde_json::from_str(event.payload()).unwrap();
313///     let update = validate_event::<UserUpdate>(payload).unwrap();
314/// });
315/// ```
316pub fn validate_event<T: VldParse>(payload: serde_json::Value) -> Result<T, VldTauriError> {
317    validate(payload)
318}
319
320/// Validate **app state / configuration** before calling `app.manage()`.
321///
322/// Returns the validated value ready to be managed.
323///
324/// # Example
325///
326/// ```rust
327/// use vld_tauri::validate_state;
328///
329/// vld::schema! {
330///     #[derive(Debug)]
331///     struct AppConfig {
332///         db_url: String => vld::string().min(1),
333///         max_connections: i64 => vld::number().int().min(1).max(100),
334///     }
335/// }
336///
337/// let cfg = serde_json::json!({"db_url": "postgres://...", "max_connections": 10});
338/// let config = validate_state::<AppConfig>(cfg).unwrap();
339/// assert_eq!(config.max_connections, 10);
340/// ```
341pub fn validate_state<T: VldParse>(value: serde_json::Value) -> Result<T, VldTauriError> {
342    validate(value)
343}
344
345/// Validate a **Tauri plugin configuration** (usually from `tauri.conf.json`).
346///
347/// # Example
348///
349/// ```rust
350/// use vld_tauri::validate_plugin_config;
351///
352/// vld::schema! {
353///     #[derive(Debug)]
354///     struct MyPluginConfig {
355///         api_key: String => vld::string().min(1),
356///         timeout: i64    => vld::number().int().min(100),
357///     }
358/// }
359///
360/// let cfg = serde_json::json!({"api_key": "abc123", "timeout": 5000});
361/// let c = validate_plugin_config::<MyPluginConfig>(cfg).unwrap();
362/// assert_eq!(c.api_key, "abc123");
363/// ```
364pub fn validate_plugin_config<T: VldParse>(config: serde_json::Value) -> Result<T, VldTauriError> {
365    validate(config)
366}
367
368/// Validate an outgoing **channel message** before sending it to the
369/// frontend via `Channel::send()`.
370///
371/// Useful for ensuring the backend only emits well-formed data.
372///
373/// # Example
374///
375/// ```rust
376/// use vld_tauri::validate_channel_message;
377///
378/// vld::schema! {
379///     #[derive(Debug)]
380///     struct Progress {
381///         percent: i64  => vld::number().int().min(0).max(100),
382///         status: String => vld::string().min(1),
383///     }
384/// }
385///
386/// let msg = serde_json::json!({"percent": 42, "status": "downloading"});
387/// let p = validate_channel_message::<Progress>(msg).unwrap();
388/// assert_eq!(p.percent, 42);
389/// ```
390pub fn validate_channel_message<T: VldParse>(
391    message: serde_json::Value,
392) -> Result<T, VldTauriError> {
393    validate(message)
394}
395
396// ---------------------------------------------------------------------------
397// VldPayload<T> — auto-validating wrapper for command args
398// ---------------------------------------------------------------------------
399
400/// Auto-validating wrapper for Tauri **command parameters**.
401///
402/// Implements [`Deserialize`] — on deserialization the incoming JSON is
403/// validated through `T::vld_parse_value()`. If validation fails,
404/// deserialization returns a `serde::de::Error` with the full message.
405///
406/// Implements [`Deref`](std::ops::Deref) to `T` so fields are accessible
407/// directly.
408///
409/// # Example
410///
411/// ```rust,ignore
412/// #[tauri::command]
413/// fn greet(payload: VldPayload<GreetArgs>) -> Result<String, VldTauriError> {
414///     Ok(format!("Hello, {}!", payload.name))
415/// }
416/// ```
417pub struct VldPayload<T>(pub T);
418
419impl<T> std::ops::Deref for VldPayload<T> {
420    type Target = T;
421    fn deref(&self) -> &T {
422        &self.0
423    }
424}
425
426impl<T> std::ops::DerefMut for VldPayload<T> {
427    fn deref_mut(&mut self) -> &mut T {
428        &mut self.0
429    }
430}
431
432impl<T: std::fmt::Debug> std::fmt::Debug for VldPayload<T> {
433    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434        f.debug_tuple("VldPayload").field(&self.0).finish()
435    }
436}
437
438impl<T: Clone> Clone for VldPayload<T> {
439    fn clone(&self) -> Self {
440        Self(self.0.clone())
441    }
442}
443
444impl<'de, T: VldParse> Deserialize<'de> for VldPayload<T> {
445    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
446    where
447        D: serde::Deserializer<'de>,
448    {
449        let value = serde_json::Value::deserialize(deserializer)?;
450        T::vld_parse_value(&value).map(VldPayload).map_err(|e| {
451            let msg = format_vld_error_inline(&e);
452            serde::de::Error::custom(msg)
453        })
454    }
455}
456
457// ---------------------------------------------------------------------------
458// VldEvent<T> — auto-validating wrapper for event payloads
459// ---------------------------------------------------------------------------
460
461/// Auto-validating wrapper for Tauri **event payloads**.
462///
463/// Works the same as [`VldPayload<T>`] but is semantically intended for
464/// data received via `Listener::listen()`.
465///
466/// # Example
467///
468/// ```rust,ignore
469/// app.listen("settings:changed", |event| {
470///     if let Ok(s) = serde_json::from_str::<VldEvent<Settings>>(event.payload()) {
471///         println!("New settings: {:?}", s.0);
472///     }
473/// });
474/// ```
475pub struct VldEvent<T>(pub T);
476
477impl<T> std::ops::Deref for VldEvent<T> {
478    type Target = T;
479    fn deref(&self) -> &T {
480        &self.0
481    }
482}
483
484impl<T> std::ops::DerefMut for VldEvent<T> {
485    fn deref_mut(&mut self) -> &mut T {
486        &mut self.0
487    }
488}
489
490impl<T: std::fmt::Debug> std::fmt::Debug for VldEvent<T> {
491    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492        f.debug_tuple("VldEvent").field(&self.0).finish()
493    }
494}
495
496impl<T: Clone> Clone for VldEvent<T> {
497    fn clone(&self) -> Self {
498        Self(self.0.clone())
499    }
500}
501
502impl<'de, T: VldParse> Deserialize<'de> for VldEvent<T> {
503    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
504    where
505        D: serde::Deserializer<'de>,
506    {
507        let value = serde_json::Value::deserialize(deserializer)?;
508        T::vld_parse_value(&value).map(VldEvent).map_err(|e| {
509            let msg = format_vld_error_inline(&e);
510            serde::de::Error::custom(msg)
511        })
512    }
513}
514
515// ---------------------------------------------------------------------------
516// Internal helpers
517// ---------------------------------------------------------------------------
518
519fn format_vld_error_inline(e: &vld::error::VldError) -> String {
520    let issues: Vec<String> = e
521        .issues
522        .iter()
523        .map(|i| {
524            let path: String = i
525                .path
526                .iter()
527                .map(|p| p.to_string())
528                .collect::<Vec<_>>()
529                .join(".");
530            format!("{path}: {}", i.message)
531        })
532        .collect();
533    format!("Validation failed: {}", issues.join("; "))
534}
535
536// ---------------------------------------------------------------------------
537// Prelude
538// ---------------------------------------------------------------------------
539
540/// Prelude — import everything you need.
541pub mod prelude {
542    pub use crate::{
543        validate, validate_args, validate_channel_message, validate_event, validate_plugin_config,
544        validate_state, TauriIssue, VldEvent, VldPayload, VldTauriError,
545    };
546    pub use vld::prelude::*;
547}