Skip to main content

vld_diesel/
lib.rs

1//! # vld-diesel — Diesel integration for the `vld` validation library
2//!
3//! Validate data **before** inserting into the database, and use strongly-typed
4//! validated column types.
5//!
6//! ## Quick Start
7//!
8//! ```ignore
9//! use vld_diesel::prelude::*;
10//!
11//! // 1. Define a vld schema for your insertable struct
12//! vld::schema! {
13//!     #[derive(Debug)]
14//!     pub struct NewUserSchema {
15//!         pub name: String  => vld::string().min(1).max(100),
16//!         pub email: String => vld::string().email(),
17//!     }
18//! }
19//!
20//! // 2. Wrap your Diesel Insertable with Validated
21//! let new_user = NewUser { name: "Alice".into(), email: "alice@example.com".into() };
22//! let validated = Validated::<NewUserSchema, _>::new(new_user)?;
23//!
24//! // 3. Insert — the inner value is guaranteed to be valid
25//! diesel::insert_into(users::table)
26//!     .values(validated.inner())
27//!     .execute(&mut conn)?;
28//! ```
29//!
30//! ## Validated column types
31//!
32//! Use [`VldText`] for columns that must always satisfy a validation schema:
33//!
34//! ```ignore
35//! use vld_diesel::VldText;
36//!
37//! let email = VldText::<EmailSchema>::new("user@example.com")?;
38//! ```
39
40use std::fmt;
41use std::ops::Deref;
42
43// ---------------------------------------------------------------------------
44// Re-exports
45// ---------------------------------------------------------------------------
46
47pub use vld;
48
49// ---------------------------------------------------------------------------
50// Validated<S, T> — wrapper that ensures T passes schema S
51// ---------------------------------------------------------------------------
52
53/// A wrapper that proves its inner value has been validated against schema `S`.
54///
55/// `S` must implement [`vld::schema::VldParse`] and `T` must be
56/// [`serde::Serialize`] so the value can be converted to JSON for validation.
57///
58/// Once constructed (via [`new`](Self::new)), the inner `T` is guaranteed valid.
59///
60/// # Example
61///
62/// ```
63/// use vld::prelude::*;
64///
65/// vld::schema! {
66///     #[derive(Debug)]
67///     pub struct NameSchema {
68///         pub name: String => vld::string().min(1).max(50),
69///     }
70/// }
71///
72/// #[derive(serde::Serialize)]
73/// struct Row { name: String }
74///
75/// let row = Row { name: "Alice".into() };
76/// let v = vld_diesel::Validated::<NameSchema, _>::new(row).unwrap();
77/// assert_eq!(v.inner().name, "Alice");
78/// ```
79pub struct Validated<S, T> {
80    inner: T,
81    _schema: std::marker::PhantomData<S>,
82}
83
84impl<S, T> Validated<S, T>
85where
86    S: vld::schema::VldParse,
87    T: serde::Serialize,
88{
89    /// Validate `value` against schema `S` and wrap it on success.
90    pub fn new(value: T) -> Result<Self, VldDieselError> {
91        let json = serde_json::to_value(&value)
92            .map_err(|e| VldDieselError::Serialization(e.to_string()))?;
93        S::vld_parse_value(&json).map_err(VldDieselError::Validation)?;
94        Ok(Self {
95            inner: value,
96            _schema: std::marker::PhantomData,
97        })
98    }
99
100    /// Get a reference to the validated inner value.
101    pub fn inner(&self) -> &T {
102        &self.inner
103    }
104
105    /// Consume and return the inner value.
106    pub fn into_inner(self) -> T {
107        self.inner
108    }
109}
110
111impl<S, T> Deref for Validated<S, T> {
112    type Target = T;
113    fn deref(&self) -> &T {
114        &self.inner
115    }
116}
117
118impl<S, T: fmt::Debug> fmt::Debug for Validated<S, T> {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.debug_struct("Validated")
121            .field("inner", &self.inner)
122            .finish()
123    }
124}
125
126impl<S, T: Clone> Clone for Validated<S, T> {
127    fn clone(&self) -> Self {
128        Self {
129            inner: self.inner.clone(),
130            _schema: std::marker::PhantomData,
131        }
132    }
133}
134
135// ---------------------------------------------------------------------------
136// validate_insert / validate_update — standalone helpers
137// ---------------------------------------------------------------------------
138
139/// Validate a value against schema `S` before inserting.
140///
141/// This is a standalone function — use it when you don't need the
142/// [`Validated`] wrapper.
143///
144/// ```
145/// use vld::prelude::*;
146///
147/// vld::schema! {
148///     #[derive(Debug)]
149///     pub struct ItemSchema {
150///         pub name: String => vld::string().min(1),
151///         pub qty: i64 => vld::number().int().min(0),
152///     }
153/// }
154///
155/// #[derive(serde::Serialize)]
156/// struct NewItem { name: String, qty: i64 }
157///
158/// let item = NewItem { name: "Widget".into(), qty: 5 };
159/// vld_diesel::validate_insert::<ItemSchema, _>(&item).unwrap();
160/// ```
161pub fn validate_insert<S, T>(value: &T) -> Result<(), VldDieselError>
162where
163    S: vld::schema::VldParse,
164    T: serde::Serialize,
165{
166    let json =
167        serde_json::to_value(value).map_err(|e| VldDieselError::Serialization(e.to_string()))?;
168    S::vld_parse_value(&json).map_err(VldDieselError::Validation)?;
169    Ok(())
170}
171
172/// Alias for [`validate_insert`] — same logic applies to updates.
173pub fn validate_update<S, T>(value: &T) -> Result<(), VldDieselError>
174where
175    S: vld::schema::VldParse,
176    T: serde::Serialize,
177{
178    validate_insert::<S, T>(value)
179}
180
181/// Validate a row loaded from the database against schema `S`.
182///
183/// Useful to enforce invariants on data that may have been inserted
184/// by other systems or before validation was in place.
185pub fn validate_row<S, T>(value: &T) -> Result<(), VldDieselError>
186where
187    S: vld::schema::VldParse,
188    T: serde::Serialize,
189{
190    validate_insert::<S, T>(value)
191}
192
193// ---------------------------------------------------------------------------
194// VldText<S> — a validated String column type
195// ---------------------------------------------------------------------------
196
197/// A validated text column type.
198///
199/// `VldText<S>` wraps a `String` and ensures it passes the `vld` schema `S`
200/// on construction. It implements Diesel's `ToSql`/`FromSql` for the `Text`
201/// SQL type, so you can use it directly in Diesel models.
202///
203/// `S` must implement [`vld::schema::VldParse`]. The schema must have a
204/// field named `value`.
205///
206/// # Example
207///
208/// ```
209/// use vld::prelude::*;
210///
211/// vld::schema! {
212///     #[derive(Debug)]
213///     pub struct EmailField {
214///         pub value: String => vld::string().email(),
215///     }
216/// }
217///
218/// let email = vld_diesel::VldText::<EmailField>::new("user@example.com").unwrap();
219/// assert_eq!(email.as_str(), "user@example.com");
220/// ```
221pub struct VldText<S> {
222    value: String,
223    _schema: std::marker::PhantomData<S>,
224}
225
226impl<S> Clone for VldText<S> {
227    fn clone(&self) -> Self {
228        Self {
229            value: self.value.clone(),
230            _schema: std::marker::PhantomData,
231        }
232    }
233}
234
235impl<S> PartialEq for VldText<S> {
236    fn eq(&self, other: &Self) -> bool {
237        self.value == other.value
238    }
239}
240impl<S> Eq for VldText<S> {}
241
242impl<S> PartialOrd for VldText<S> {
243    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
244        Some(self.cmp(other))
245    }
246}
247impl<S> Ord for VldText<S> {
248    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
249        self.value.cmp(&other.value)
250    }
251}
252
253impl<S> std::hash::Hash for VldText<S> {
254    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
255        self.value.hash(state);
256    }
257}
258
259impl<S: vld::schema::VldParse> VldText<S> {
260    /// Create a validated text value.
261    ///
262    /// The `input` is wrapped in `{"value": "..."}` and validated against `S`.
263    pub fn new(input: impl Into<String>) -> Result<Self, VldDieselError> {
264        let s = input.into();
265        let json = serde_json::json!({ "value": s });
266        S::vld_parse_value(&json).map_err(VldDieselError::Validation)?;
267        Ok(Self {
268            value: s,
269            _schema: std::marker::PhantomData,
270        })
271    }
272
273    /// Create without validation (e.g. for data loaded from a trusted DB).
274    pub fn new_unchecked(input: impl Into<String>) -> Self {
275        Self {
276            value: input.into(),
277            _schema: std::marker::PhantomData,
278        }
279    }
280
281    /// Get the inner string.
282    pub fn as_str(&self) -> &str {
283        &self.value
284    }
285
286    /// Consume and return the inner `String`.
287    pub fn into_inner(self) -> String {
288        self.value
289    }
290}
291
292impl<S> Deref for VldText<S> {
293    type Target = str;
294    fn deref(&self) -> &str {
295        &self.value
296    }
297}
298
299impl<S> AsRef<str> for VldText<S> {
300    fn as_ref(&self) -> &str {
301        &self.value
302    }
303}
304
305impl<S> fmt::Debug for VldText<S> {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        write!(f, "VldText({:?})", self.value)
308    }
309}
310
311impl<S> fmt::Display for VldText<S> {
312    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313        f.write_str(&self.value)
314    }
315}
316
317impl<S> serde::Serialize for VldText<S> {
318    fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
319        self.value.serialize(serializer)
320    }
321}
322
323impl<'de, S: vld::schema::VldParse> serde::Deserialize<'de> for VldText<S> {
324    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
325        let s = String::deserialize(deserializer)?;
326        VldText::<S>::new(&s).map_err(serde::de::Error::custom)
327    }
328}
329
330// Diesel ToSql / FromSql for VldText — maps to diesel::sql_types::Text.
331// We delegate to the underlying String / &str.
332
333impl<S, DB> diesel::serialize::ToSql<diesel::sql_types::Text, DB> for VldText<S>
334where
335    DB: diesel::backend::Backend,
336    str: diesel::serialize::ToSql<diesel::sql_types::Text, DB>,
337{
338    fn to_sql<'b>(
339        &'b self,
340        out: &mut diesel::serialize::Output<'b, '_, DB>,
341    ) -> diesel::serialize::Result {
342        <str as diesel::serialize::ToSql<diesel::sql_types::Text, DB>>::to_sql(&self.value, out)
343    }
344}
345
346impl<S: vld::schema::VldParse, DB> diesel::deserialize::FromSql<diesel::sql_types::Text, DB>
347    for VldText<S>
348where
349    DB: diesel::backend::Backend,
350    String: diesel::deserialize::FromSql<diesel::sql_types::Text, DB>,
351{
352    fn from_sql(
353        bytes: <DB as diesel::backend::Backend>::RawValue<'_>,
354    ) -> diesel::deserialize::Result<Self> {
355        let s =
356            <String as diesel::deserialize::FromSql<diesel::sql_types::Text, DB>>::from_sql(bytes)?;
357        Ok(Self::new_unchecked(s))
358    }
359}
360
361// ---------------------------------------------------------------------------
362// VldInt<S> — a validated integer column type
363// ---------------------------------------------------------------------------
364
365/// A validated integer column type.
366///
367/// Wraps an `i64` and ensures it passes the `vld` schema `S` on construction.
368/// The schema must have a field named `value`.
369///
370/// # Example
371///
372/// ```
373/// use vld::prelude::*;
374///
375/// vld::schema! {
376///     #[derive(Debug)]
377///     pub struct AgeField {
378///         pub value: i64 => vld::number().int().min(0).max(150),
379///     }
380/// }
381///
382/// let age = vld_diesel::VldInt::<AgeField>::new(25).unwrap();
383/// assert_eq!(*age, 25);
384/// assert!(vld_diesel::VldInt::<AgeField>::new(-1).is_err());
385/// ```
386pub struct VldInt<S> {
387    value: i64,
388    _schema: std::marker::PhantomData<S>,
389}
390
391impl<S> Clone for VldInt<S> {
392    fn clone(&self) -> Self {
393        *self
394    }
395}
396impl<S> Copy for VldInt<S> {}
397
398impl<S> PartialEq for VldInt<S> {
399    fn eq(&self, other: &Self) -> bool {
400        self.value == other.value
401    }
402}
403impl<S> Eq for VldInt<S> {}
404
405impl<S> PartialOrd for VldInt<S> {
406    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
407        Some(self.cmp(other))
408    }
409}
410impl<S> Ord for VldInt<S> {
411    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
412        self.value.cmp(&other.value)
413    }
414}
415
416impl<S> std::hash::Hash for VldInt<S> {
417    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
418        self.value.hash(state);
419    }
420}
421
422impl<S: vld::schema::VldParse> VldInt<S> {
423    /// Create a validated integer.
424    pub fn new(input: i64) -> Result<Self, VldDieselError> {
425        let json = serde_json::json!({ "value": input });
426        S::vld_parse_value(&json).map_err(VldDieselError::Validation)?;
427        Ok(Self {
428            value: input,
429            _schema: std::marker::PhantomData,
430        })
431    }
432
433    /// Create without validation.
434    pub fn new_unchecked(input: i64) -> Self {
435        Self {
436            value: input,
437            _schema: std::marker::PhantomData,
438        }
439    }
440
441    /// Get the inner value.
442    pub fn get(&self) -> i64 {
443        self.value
444    }
445}
446
447impl<S> Deref for VldInt<S> {
448    type Target = i64;
449    fn deref(&self) -> &i64 {
450        &self.value
451    }
452}
453
454impl<S> fmt::Debug for VldInt<S> {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        write!(f, "VldInt({})", self.value)
457    }
458}
459
460impl<S> fmt::Display for VldInt<S> {
461    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
462        write!(f, "{}", self.value)
463    }
464}
465
466impl<S> serde::Serialize for VldInt<S> {
467    fn serialize<Ser: serde::Serializer>(&self, serializer: Ser) -> Result<Ser::Ok, Ser::Error> {
468        self.value.serialize(serializer)
469    }
470}
471
472impl<'de, S: vld::schema::VldParse> serde::Deserialize<'de> for VldInt<S> {
473    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
474        let v = i64::deserialize(deserializer)?;
475        VldInt::<S>::new(v).map_err(serde::de::Error::custom)
476    }
477}
478
479// Diesel ToSql / FromSql for VldInt — maps to diesel::sql_types::BigInt (i64).
480
481impl<S, DB> diesel::serialize::ToSql<diesel::sql_types::BigInt, DB> for VldInt<S>
482where
483    DB: diesel::backend::Backend,
484    i64: diesel::serialize::ToSql<diesel::sql_types::BigInt, DB>,
485{
486    fn to_sql<'b>(
487        &'b self,
488        out: &mut diesel::serialize::Output<'b, '_, DB>,
489    ) -> diesel::serialize::Result {
490        <i64 as diesel::serialize::ToSql<diesel::sql_types::BigInt, DB>>::to_sql(&self.value, out)
491    }
492}
493
494impl<S: vld::schema::VldParse, DB> diesel::deserialize::FromSql<diesel::sql_types::BigInt, DB>
495    for VldInt<S>
496where
497    DB: diesel::backend::Backend,
498    i64: diesel::deserialize::FromSql<diesel::sql_types::BigInt, DB>,
499{
500    fn from_sql(
501        bytes: <DB as diesel::backend::Backend>::RawValue<'_>,
502    ) -> diesel::deserialize::Result<Self> {
503        let v =
504            <i64 as diesel::deserialize::FromSql<diesel::sql_types::BigInt, DB>>::from_sql(bytes)?;
505        Ok(Self::new_unchecked(v))
506    }
507}
508
509// FromSql for Integer (i32) — useful when the DB column is INTEGER.
510
511impl<S: vld::schema::VldParse, DB> diesel::deserialize::FromSql<diesel::sql_types::Integer, DB>
512    for VldInt<S>
513where
514    DB: diesel::backend::Backend,
515    i32: diesel::deserialize::FromSql<diesel::sql_types::Integer, DB>,
516{
517    fn from_sql(
518        bytes: <DB as diesel::backend::Backend>::RawValue<'_>,
519    ) -> diesel::deserialize::Result<Self> {
520        let v =
521            <i32 as diesel::deserialize::FromSql<diesel::sql_types::Integer, DB>>::from_sql(bytes)?;
522        Ok(Self::new_unchecked(v as i64))
523    }
524}
525
526// ---------------------------------------------------------------------------
527// Error type
528// ---------------------------------------------------------------------------
529
530/// Error returned by `vld-diesel` operations.
531#[derive(Debug, Clone)]
532pub enum VldDieselError {
533    /// Schema validation failed.
534    Validation(vld::error::VldError),
535    /// Failed to serialize the value to JSON for validation.
536    Serialization(String),
537}
538
539impl fmt::Display for VldDieselError {
540    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
541        match self {
542            VldDieselError::Validation(e) => write!(f, "Validation error: {}", e),
543            VldDieselError::Serialization(e) => write!(f, "Serialization error: {}", e),
544        }
545    }
546}
547
548impl std::error::Error for VldDieselError {
549    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
550        match self {
551            VldDieselError::Validation(e) => Some(e),
552            VldDieselError::Serialization(_) => None,
553        }
554    }
555}
556
557impl From<vld::error::VldError> for VldDieselError {
558    fn from(e: vld::error::VldError) -> Self {
559        VldDieselError::Validation(e)
560    }
561}
562
563// ---------------------------------------------------------------------------
564// Prelude
565// ---------------------------------------------------------------------------
566
567pub mod prelude {
568    pub use crate::{
569        validate_insert, validate_row, validate_update, Validated, VldDieselError, VldInt, VldText,
570    };
571    pub use vld::prelude::*;
572}