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}