Skip to main content

vld_sea/
lib.rs

1//! # vld-sea — SeaORM integration for `vld`
2//!
3//! Validate [`ActiveModel`](sea_orm::ActiveModelTrait) fields **before**
4//! `insert()` / `update()` hits the database.
5//!
6//! ## Approach
7//!
8//! 1. Define a `vld::schema!` that mirrors the entity columns you want validated.
9//! 2. Call [`validate_active`] (extracts `Set`/`Unchanged` values from the
10//!    ActiveModel into JSON and runs the schema).
11//! 3. Or call [`validate_model`] on any `Serialize`-able struct (e.g. an input
12//!    DTO, or SeaORM `Model`).
13//! 4. Optionally hook into
14//!    [`ActiveModelBehavior::before_save`](sea_orm::ActiveModelBehavior::before_save)
15//!    so validation runs automatically.
16//!
17//! ## Quick Start
18//!
19//! ```rust,ignore
20//! use sea_orm::*;
21//! use vld_sea::prelude::*;
22//!
23//! vld::schema! {
24//!     #[derive(Debug)]
25//!     pub struct UserInput {
26//!         pub name: String  => vld::string().min(1).max(100),
27//!         pub email: String => vld::string().email(),
28//!     }
29//! }
30//!
31//! // Before insert:
32//! let am = user::ActiveModel {
33//!     name: Set("Alice".to_owned()),
34//!     email: Set("alice@example.com".to_owned()),
35//!     ..Default::default()
36//! };
37//! vld_sea::validate_active::<UserInput, _>(&am)?;
38//! am.insert(&db).await?;
39//! ```
40
41use std::fmt;
42use std::ops::Deref;
43
44pub use sea_orm;
45pub use vld;
46
47// ---------------------------------------------------------------------------
48// Error type
49// ---------------------------------------------------------------------------
50
51/// Error returned by `vld-sea` operations.
52#[derive(Debug, Clone)]
53pub enum VldSeaError {
54    /// Schema validation failed.
55    Validation(vld::error::VldError),
56    /// Failed to serialize the value to JSON for validation.
57    Serialization(String),
58}
59
60impl fmt::Display for VldSeaError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        match self {
63            VldSeaError::Validation(e) => write!(f, "Validation error: {}", e),
64            VldSeaError::Serialization(e) => write!(f, "Serialization error: {}", e),
65        }
66    }
67}
68
69impl std::error::Error for VldSeaError {
70    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
71        match self {
72            VldSeaError::Validation(e) => Some(e),
73            VldSeaError::Serialization(_) => None,
74        }
75    }
76}
77
78impl From<vld::error::VldError> for VldSeaError {
79    fn from(e: vld::error::VldError) -> Self {
80        VldSeaError::Validation(e)
81    }
82}
83
84impl From<VldSeaError> for sea_orm::DbErr {
85    fn from(e: VldSeaError) -> Self {
86        sea_orm::DbErr::Custom(e.to_string())
87    }
88}
89
90// ---------------------------------------------------------------------------
91// ActiveModel → JSON conversion
92// ---------------------------------------------------------------------------
93
94/// Convert an [`ActiveModel`](sea_orm::ActiveModelTrait) to a
95/// [`serde_json::Value`] object.
96///
97/// Only fields with `Set` or `Unchanged` values are included.
98/// Fields that are `NotSet` are omitted from the resulting object.
99///
100/// This function handles common SQL types (bool, integers, floats, strings,
101/// chars). Feature-gated types (chrono, uuid, etc.) are mapped to
102/// `serde_json::Value::Null`.
103pub fn active_model_to_json<A>(model: &A) -> serde_json::Value
104where
105    A: sea_orm::ActiveModelTrait,
106{
107    use sea_orm::{ActiveValue, EntityTrait, IdenStatic, Iterable};
108
109    let mut map = serde_json::Map::new();
110    for col in <<A as sea_orm::ActiveModelTrait>::Entity as EntityTrait>::Column::iter() {
111        match model.get(col) {
112            ActiveValue::Set(v) | ActiveValue::Unchanged(v) => {
113                map.insert(col.as_str().to_string(), sea_value_to_json(v));
114            }
115            ActiveValue::NotSet => {} // skip — not being set
116        }
117    }
118    serde_json::Value::Object(map)
119}
120
121/// Convert a [`sea_orm::Value`] to [`serde_json::Value`].
122///
123/// Handles the always-available variants. Feature-gated variants
124/// (chrono, uuid, json, etc.) fall through to `Null`.
125fn sea_value_to_json(v: sea_orm::Value) -> serde_json::Value {
126    use sea_orm::sea_query::Value as SV;
127
128    match v {
129        SV::Bool(Some(b)) => serde_json::Value::Bool(b),
130        SV::TinyInt(Some(n)) => serde_json::Value::Number((n as i64).into()),
131        SV::SmallInt(Some(n)) => serde_json::Value::Number((n as i64).into()),
132        SV::Int(Some(n)) => serde_json::Value::Number((n as i64).into()),
133        SV::BigInt(Some(n)) => serde_json::Value::Number(n.into()),
134        SV::TinyUnsigned(Some(n)) => serde_json::Value::Number((n as u64).into()),
135        SV::SmallUnsigned(Some(n)) => serde_json::Value::Number((n as u64).into()),
136        SV::Unsigned(Some(n)) => serde_json::Value::Number((n as u64).into()),
137        SV::BigUnsigned(Some(n)) => serde_json::Value::Number(n.into()),
138        SV::Float(Some(n)) => serde_json::Number::from_f64(n as f64)
139            .map(serde_json::Value::Number)
140            .unwrap_or(serde_json::Value::Null),
141        SV::Double(Some(n)) => serde_json::Number::from_f64(n)
142            .map(serde_json::Value::Number)
143            .unwrap_or(serde_json::Value::Null),
144        SV::String(Some(s)) => serde_json::Value::String(*s),
145        SV::Char(Some(c)) => serde_json::Value::String(c.to_string()),
146        // Bytes, Chrono, Uuid, Json, etc. — or any None variant
147        _ => serde_json::Value::Null,
148    }
149}
150
151// ---------------------------------------------------------------------------
152// Validation functions
153// ---------------------------------------------------------------------------
154
155/// Validate an [`ActiveModel`](sea_orm::ActiveModelTrait) against schema `S`.
156///
157/// Extracts all `Set` and `Unchanged` fields into a JSON object and runs
158/// `S::vld_parse_value()`.
159///
160/// Use this in [`ActiveModelBehavior::before_save`](sea_orm::ActiveModelBehavior::before_save)
161/// to validate before every insert/update.
162///
163/// ```rust,ignore
164/// vld_sea::validate_active::<UserInput, _>(&active_model)?;
165/// ```
166pub fn validate_active<S, A>(model: &A) -> Result<S, VldSeaError>
167where
168    S: vld::schema::VldParse,
169    A: sea_orm::ActiveModelTrait,
170{
171    let json = active_model_to_json(model);
172    S::vld_parse_value(&json).map_err(VldSeaError::Validation)
173}
174
175/// Validate a serializable value against schema `S`.
176///
177/// Works with SeaORM `Model`, input DTOs, or any `Serialize`-able struct.
178///
179/// ```rust,ignore
180/// #[derive(serde::Serialize)]
181/// struct NewUser { name: String, email: String }
182///
183/// let input = NewUser { name: "Alice".into(), email: "alice@example.com".into() };
184/// vld_sea::validate_model::<UserInput, _>(&input)?;
185/// ```
186pub fn validate_model<S, T>(value: &T) -> Result<S, VldSeaError>
187where
188    S: vld::schema::VldParse,
189    T: serde::Serialize,
190{
191    let json =
192        serde_json::to_value(value).map_err(|e| VldSeaError::Serialization(e.to_string()))?;
193    S::vld_parse_value(&json).map_err(VldSeaError::Validation)
194}
195
196/// Parse and validate raw JSON against schema `S`.
197pub fn validate_json<S>(json: &serde_json::Value) -> Result<S, VldSeaError>
198where
199    S: vld::schema::VldParse,
200{
201    S::vld_parse_value(json).map_err(VldSeaError::Validation)
202}
203
204/// Helper for use inside
205/// [`ActiveModelBehavior::before_save`](sea_orm::ActiveModelBehavior::before_save).
206///
207/// Returns `Ok(())` on success or `Err(DbErr::Custom(...))` on failure,
208/// so it can be used directly with `?` in the `before_save` method.
209///
210/// ```rust,ignore
211/// #[async_trait::async_trait]
212/// impl ActiveModelBehavior for ActiveModel {
213///     async fn before_save<C: ConnectionTrait>(
214///         self, _db: &C, _insert: bool,
215///     ) -> Result<Self, DbErr> {
216///         vld_sea::before_save::<UserInput, _>(&self)?;
217///         Ok(self)
218///     }
219/// }
220/// ```
221pub fn before_save<S, A>(model: &A) -> Result<(), sea_orm::DbErr>
222where
223    S: vld::schema::VldParse,
224    A: sea_orm::ActiveModelTrait,
225{
226    let json = active_model_to_json(model);
227    S::vld_parse_value(&json)
228        .map(|_| ())
229        .map_err(|e| sea_orm::DbErr::Custom(format!("Validation error: {}", e)))
230}
231
232// ---------------------------------------------------------------------------
233// Validated<S, T> — wrapper
234// ---------------------------------------------------------------------------
235
236/// A wrapper that proves its inner value has been validated against schema `S`.
237///
238/// `T` must implement [`serde::Serialize`] so it can be converted to JSON
239/// for validation.
240///
241/// ```rust,ignore
242/// let input = NewUser { name: "Alice".into(), email: "alice@example.com".into() };
243/// let validated = vld_sea::Validated::<UserInput, _>::new(input)?;
244/// // validated.inner() is guaranteed valid
245/// ```
246pub struct Validated<S, T> {
247    inner: T,
248    _schema: std::marker::PhantomData<S>,
249}
250
251impl<S, T> Validated<S, T>
252where
253    S: vld::schema::VldParse,
254    T: serde::Serialize,
255{
256    /// Validate `value` against schema `S` and wrap it on success.
257    pub fn new(value: T) -> Result<Self, VldSeaError> {
258        let json =
259            serde_json::to_value(&value).map_err(|e| VldSeaError::Serialization(e.to_string()))?;
260        S::vld_parse_value(&json).map_err(VldSeaError::Validation)?;
261        Ok(Self {
262            inner: value,
263            _schema: std::marker::PhantomData,
264        })
265    }
266
267    /// Get a reference to the validated inner value.
268    pub fn inner(&self) -> &T {
269        &self.inner
270    }
271
272    /// Consume and return the inner value.
273    pub fn into_inner(self) -> T {
274        self.inner
275    }
276}
277
278impl<S, T> Deref for Validated<S, T> {
279    type Target = T;
280    fn deref(&self) -> &T {
281        &self.inner
282    }
283}
284
285impl<S, T: fmt::Debug> fmt::Debug for Validated<S, T> {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        f.debug_struct("Validated")
288            .field("inner", &self.inner)
289            .finish()
290    }
291}
292
293impl<S, T: Clone> Clone for Validated<S, T> {
294    fn clone(&self) -> Self {
295        Self {
296            inner: self.inner.clone(),
297            _schema: std::marker::PhantomData,
298        }
299    }
300}
301
302// ---------------------------------------------------------------------------
303// Macro: impl_vld_before_save!
304// ---------------------------------------------------------------------------
305
306/// Implements [`ActiveModelBehavior`](sea_orm::ActiveModelBehavior) with
307/// automatic vld validation in `before_save`.
308///
309/// ```rust,ignore
310/// // In your entity module:
311/// vld_sea::impl_vld_before_save!(ActiveModel, UserInput);
312/// ```
313///
314/// This expands to an `ActiveModelBehavior` impl that calls
315/// [`validate_active`] before every insert/update.
316#[macro_export]
317macro_rules! impl_vld_before_save {
318    ($active_model:ty, $schema:ty) => {
319        #[sea_orm::prelude::async_trait::async_trait]
320        impl sea_orm::ActiveModelBehavior for $active_model {
321            async fn before_save<C: sea_orm::ConnectionTrait>(
322                self,
323                _db: &C,
324                _insert: bool,
325            ) -> Result<Self, sea_orm::DbErr> {
326                $crate::before_save::<$schema, _>(&self)?;
327                Ok(self)
328            }
329        }
330    };
331    ($active_model:ty, insert: $ins_schema:ty, update: $upd_schema:ty) => {
332        #[sea_orm::prelude::async_trait::async_trait]
333        impl sea_orm::ActiveModelBehavior for $active_model {
334            async fn before_save<C: sea_orm::ConnectionTrait>(
335                self,
336                _db: &C,
337                insert: bool,
338            ) -> Result<Self, sea_orm::DbErr> {
339                if insert {
340                    $crate::before_save::<$ins_schema, _>(&self)?;
341                } else {
342                    $crate::before_save::<$upd_schema, _>(&self)?;
343                }
344                Ok(self)
345            }
346        }
347    };
348}
349
350// ---------------------------------------------------------------------------
351// Prelude
352// ---------------------------------------------------------------------------
353
354/// Prelude — import everything you need.
355pub mod prelude {
356    pub use crate::{
357        active_model_to_json, before_save, validate_active, validate_json, validate_model,
358        Validated, VldSeaError,
359    };
360    pub use vld::prelude::*;
361}