Skip to main content

karbon_framework/validation/
async_validator.rs

1use crate::db::{DbPool, placeholder};
2use crate::error::{AppError, AppResult};
3
4/// Validateur asynchrone pour les règles qui nécessitent la base de données.
5///
6/// # Exemples
7///
8/// ```ignore
9/// // Création — vérifier l'unicité
10/// AsyncValidator::new(pool)
11///     .unique("users", "username", &input.username, "Ce nom d'utilisateur est déjà pris")
12///     .unique("users", "email", &input.email, "Cette adresse email est déjà utilisée")
13///     .validate()
14///     .await?;
15///
16/// // Mise à jour — exclure l'ID courant
17/// AsyncValidator::new(pool)
18///     .unique_except("users", "username", &input.username, user_id, "Ce nom d'utilisateur est déjà pris")
19///     .unique_except("users", "email", &input.email, user_id, "Cette adresse email est déjà utilisée")
20///     .validate()
21///     .await?;
22///
23/// // Vérifier qu'une FK existe
24/// AsyncValidator::new(pool)
25///     .exists("category", "id", category_id, "Catégorie introuvable")
26///     .validate()
27///     .await?;
28/// ```
29pub struct AsyncValidator<'a> {
30    pool: &'a DbPool,
31    rules: Vec<ValidationRule>,
32}
33
34enum ValidationRule {
35    Unique {
36        table: String,
37        column: String,
38        value: String,
39        except_id: Option<i64>,
40        message: String,
41    },
42    Exists {
43        table: String,
44        column: String,
45        value: i64,
46        message: String,
47    },
48}
49
50impl<'a> AsyncValidator<'a> {
51    pub fn new(pool: &'a DbPool) -> Self {
52        Self {
53            pool,
54            rules: Vec::new(),
55        }
56    }
57
58    /// Vérifie qu'une valeur est unique dans une table.
59    /// Retourne `Conflict` si la valeur existe déjà.
60    pub fn unique(mut self, table: &str, column: &str, value: &str, message: &str) -> Self {
61        self.rules.push(ValidationRule::Unique {
62            table: table.to_string(),
63            column: column.to_string(),
64            value: value.to_string(),
65            except_id: None,
66            message: message.to_string(),
67        });
68        self
69    }
70
71    /// Vérifie qu'une valeur est unique, en excluant un ID (pour les mises à jour).
72    /// Retourne `Conflict` si un AUTRE enregistrement a déjà cette valeur.
73    pub fn unique_except(mut self, table: &str, column: &str, value: &str, except_id: i64, message: &str) -> Self {
74        self.rules.push(ValidationRule::Unique {
75            table: table.to_string(),
76            column: column.to_string(),
77            value: value.to_string(),
78            except_id: Some(except_id),
79            message: message.to_string(),
80        });
81        self
82    }
83
84    /// Comme `unique_except`, mais uniquement si la valeur est `Some`.
85    /// Utile pour les mises à jour partielles avec des champs optionnels.
86    pub fn unique_except_if_some(self, table: &str, column: &str, value: &Option<String>, except_id: i64, message: &str) -> Self {
87        match value {
88            Some(v) => self.unique_except(table, column, v, except_id, message),
89            None => self,
90        }
91    }
92
93    /// Vérifie qu'un enregistrement existe (ex: FK valide).
94    /// Retourne `NotFound` si l'enregistrement n'existe pas.
95    pub fn exists(mut self, table: &str, column: &str, value: i64, message: &str) -> Self {
96        self.rules.push(ValidationRule::Exists {
97            table: table.to_string(),
98            column: column.to_string(),
99            value,
100            message: message.to_string(),
101        });
102        self
103    }
104
105    /// Exécute toutes les règles. Retourne la première erreur rencontrée.
106    pub async fn validate(self) -> AppResult<()> {
107        for rule in &self.rules {
108            match rule {
109                ValidationRule::Unique { table, column, value, except_id, message } => {
110                    let sql = match except_id {
111                        Some(_) => format!(
112                            "SELECT COUNT(*) FROM {} WHERE {} = {} AND id != {}",
113                            table, column, placeholder(1), placeholder(2)
114                        ),
115                        None => format!(
116                            "SELECT COUNT(*) FROM {} WHERE {} = {}",
117                            table, column, placeholder(1)
118                        ),
119                    };
120
121                    let mut query = sqlx::query_as::<_, (i64,)>(&sql).bind(value);
122                    if let Some(id) = except_id {
123                        query = query.bind(*id);
124                    }
125
126                    let (count,) = query.fetch_one(self.pool).await?;
127                    if count > 0 {
128                        return Err(AppError::Conflict(message.clone()));
129                    }
130                }
131                ValidationRule::Exists { table, column, value, message } => {
132                    let sql = format!(
133                        "SELECT COUNT(*) FROM {} WHERE {} = {}",
134                        table, column, placeholder(1)
135                    );
136                    let (count,): (i64,) = sqlx::query_as(&sql)
137                        .bind(*value)
138                        .fetch_one(self.pool)
139                        .await?;
140                    if count == 0 {
141                        return Err(AppError::NotFound(message.clone()));
142                    }
143                }
144            }
145        }
146        Ok(())
147    }
148}