rustapi_validate/v2/rules/
async_rules.rs

1//! Asynchronous validation rules.
2//!
3//! These rules require async operations like database queries or API calls.
4
5use crate::v2::context::ValidationContext;
6use crate::v2::error::RuleError;
7use crate::v2::traits::AsyncValidationRule;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11/// Database uniqueness validation rule.
12///
13/// Validates that a value is unique in a database table column.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct AsyncUniqueRule {
16    /// Database table name
17    pub table: String,
18    /// Column name to check
19    pub column: String,
20    /// Custom error message
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub message: Option<String>,
23}
24
25impl AsyncUniqueRule {
26    /// Create a new uniqueness rule.
27    pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
28        Self {
29            table: table.into(),
30            column: column.into(),
31            message: None,
32        }
33    }
34
35    /// Set a custom error message.
36    pub fn with_message(mut self, message: impl Into<String>) -> Self {
37        self.message = Some(message.into());
38        self
39    }
40}
41
42#[async_trait]
43impl AsyncValidationRule<str> for AsyncUniqueRule {
44    async fn validate_async(&self, value: &str, ctx: &ValidationContext) -> Result<(), RuleError> {
45        let db = ctx.database().ok_or_else(|| {
46            RuleError::new(
47                "async_unique",
48                "Database validator not configured in context",
49            )
50        })?;
51
52        let is_unique = if let Some(exclude_id) = ctx.exclude_id() {
53            db.is_unique_except(&self.table, &self.column, value, exclude_id)
54                .await
55                .map_err(|e| RuleError::new("async_unique", format!("Database error: {}", e)))?
56        } else {
57            db.is_unique(&self.table, &self.column, value)
58                .await
59                .map_err(|e| RuleError::new("async_unique", format!("Database error: {}", e)))?
60        };
61
62        if is_unique {
63            Ok(())
64        } else {
65            let message = self
66                .message
67                .clone()
68                .unwrap_or_else(|| "validation.unique.taken".to_string());
69            Err(RuleError::new("async_unique", message)
70                .param("table", self.table.clone())
71                .param("column", self.column.clone()))
72        }
73    }
74
75    fn rule_name(&self) -> &'static str {
76        "async_unique"
77    }
78}
79
80#[async_trait]
81impl AsyncValidationRule<String> for AsyncUniqueRule {
82    async fn validate_async(
83        &self,
84        value: &String,
85        ctx: &ValidationContext,
86    ) -> Result<(), RuleError> {
87        <Self as AsyncValidationRule<str>>::validate_async(self, value.as_str(), ctx).await
88    }
89
90    fn rule_name(&self) -> &'static str {
91        "async_unique"
92    }
93}
94
95/// Database existence validation rule.
96///
97/// Validates that a value exists in a database table column.
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99pub struct AsyncExistsRule {
100    /// Database table name
101    pub table: String,
102    /// Column name to check
103    pub column: String,
104    /// Custom error message
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub message: Option<String>,
107}
108
109impl AsyncExistsRule {
110    /// Create a new existence rule.
111    pub fn new(table: impl Into<String>, column: impl Into<String>) -> Self {
112        Self {
113            table: table.into(),
114            column: column.into(),
115            message: None,
116        }
117    }
118
119    /// Set a custom error message.
120    pub fn with_message(mut self, message: impl Into<String>) -> Self {
121        self.message = Some(message.into());
122        self
123    }
124}
125
126#[async_trait]
127impl AsyncValidationRule<str> for AsyncExistsRule {
128    async fn validate_async(&self, value: &str, ctx: &ValidationContext) -> Result<(), RuleError> {
129        let db = ctx.database().ok_or_else(|| {
130            RuleError::new(
131                "async_exists",
132                "Database validator not configured in context",
133            )
134        })?;
135
136        let exists = db
137            .exists(&self.table, &self.column, value)
138            .await
139            .map_err(|e| RuleError::new("async_exists", format!("Database error: {}", e)))?;
140
141        if exists {
142            Ok(())
143        } else {
144            let message = self
145                .message
146                .clone()
147                .unwrap_or_else(|| "validation.exists.not_found".to_string());
148            Err(RuleError::new("async_exists", message)
149                .param("table", self.table.clone())
150                .param("column", self.column.clone()))
151        }
152    }
153
154    fn rule_name(&self) -> &'static str {
155        "async_exists"
156    }
157}
158
159#[async_trait]
160impl AsyncValidationRule<String> for AsyncExistsRule {
161    async fn validate_async(
162        &self,
163        value: &String,
164        ctx: &ValidationContext,
165    ) -> Result<(), RuleError> {
166        <Self as AsyncValidationRule<str>>::validate_async(self, value.as_str(), ctx).await
167    }
168
169    fn rule_name(&self) -> &'static str {
170        "async_exists"
171    }
172}
173
174/// External API validation rule.
175///
176/// Validates a value against an external API endpoint.
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178pub struct AsyncApiRule {
179    /// API endpoint URL
180    pub endpoint: String,
181    /// Custom error message
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub message: Option<String>,
184}
185
186impl AsyncApiRule {
187    /// Create a new API validation rule.
188    pub fn new(endpoint: impl Into<String>) -> Self {
189        Self {
190            endpoint: endpoint.into(),
191            message: None,
192        }
193    }
194
195    /// Set a custom error message.
196    pub fn with_message(mut self, message: impl Into<String>) -> Self {
197        self.message = Some(message.into());
198        self
199    }
200}
201
202#[async_trait]
203impl AsyncValidationRule<str> for AsyncApiRule {
204    async fn validate_async(&self, value: &str, ctx: &ValidationContext) -> Result<(), RuleError> {
205        let http = ctx.http().ok_or_else(|| {
206            RuleError::new("async_api", "HTTP validator not configured in context")
207        })?;
208
209        let is_valid = http
210            .validate(&self.endpoint, value)
211            .await
212            .map_err(|e| RuleError::new("async_api", format!("API error: {}", e)))?;
213
214        if is_valid {
215            Ok(())
216        } else {
217            let message = self
218                .message
219                .clone()
220                .unwrap_or_else(|| "validation.api.invalid".to_string());
221            Err(RuleError::new("async_api", message).param("endpoint", self.endpoint.clone()))
222        }
223    }
224
225    fn rule_name(&self) -> &'static str {
226        "async_api"
227    }
228}
229
230#[async_trait]
231impl AsyncValidationRule<String> for AsyncApiRule {
232    async fn validate_async(
233        &self,
234        value: &String,
235        ctx: &ValidationContext,
236    ) -> Result<(), RuleError> {
237        <Self as AsyncValidationRule<str>>::validate_async(self, value.as_str(), ctx).await
238    }
239
240    fn rule_name(&self) -> &'static str {
241        "async_api"
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::v2::context::{DatabaseValidator, ValidationContextBuilder};
249
250    struct MockDbValidator {
251        unique_values: Vec<String>,
252        existing_values: Vec<String>,
253    }
254
255    #[async_trait]
256    impl DatabaseValidator for MockDbValidator {
257        async fn exists(&self, _table: &str, _column: &str, value: &str) -> Result<bool, String> {
258            Ok(self.existing_values.contains(&value.to_string()))
259        }
260
261        async fn is_unique(
262            &self,
263            _table: &str,
264            _column: &str,
265            value: &str,
266        ) -> Result<bool, String> {
267            Ok(!self.unique_values.contains(&value.to_string()))
268        }
269
270        async fn is_unique_except(
271            &self,
272            _table: &str,
273            _column: &str,
274            value: &str,
275            _except_id: &str,
276        ) -> Result<bool, String> {
277            Ok(!self.unique_values.contains(&value.to_string()))
278        }
279    }
280
281    #[tokio::test]
282    async fn async_unique_rule_valid() {
283        let db = MockDbValidator {
284            unique_values: vec!["taken@example.com".to_string()],
285            existing_values: vec![],
286        };
287        let ctx = ValidationContextBuilder::new().database(db).build();
288
289        let rule = AsyncUniqueRule::new("users", "email");
290        assert!(rule.validate_async("new@example.com", &ctx).await.is_ok());
291    }
292
293    #[tokio::test]
294    async fn async_unique_rule_invalid() {
295        let db = MockDbValidator {
296            unique_values: vec!["taken@example.com".to_string()],
297            existing_values: vec![],
298        };
299        let ctx = ValidationContextBuilder::new().database(db).build();
300
301        let rule = AsyncUniqueRule::new("users", "email");
302        let err = rule
303            .validate_async("taken@example.com", &ctx)
304            .await
305            .unwrap_err();
306        assert_eq!(err.code, "async_unique");
307    }
308
309    #[tokio::test]
310    async fn async_exists_rule_valid() {
311        let db = MockDbValidator {
312            unique_values: vec![],
313            existing_values: vec!["existing_id".to_string()],
314        };
315        let ctx = ValidationContextBuilder::new().database(db).build();
316
317        let rule = AsyncExistsRule::new("users", "id");
318        assert!(rule.validate_async("existing_id", &ctx).await.is_ok());
319    }
320
321    #[tokio::test]
322    async fn async_exists_rule_invalid() {
323        let db = MockDbValidator {
324            unique_values: vec![],
325            existing_values: vec!["existing_id".to_string()],
326        };
327        let ctx = ValidationContextBuilder::new().database(db).build();
328
329        let rule = AsyncExistsRule::new("users", "id");
330        let err = rule
331            .validate_async("nonexistent_id", &ctx)
332            .await
333            .unwrap_err();
334        assert_eq!(err.code, "async_exists");
335    }
336
337    #[tokio::test]
338    async fn async_rule_without_context() {
339        let ctx = ValidationContext::new();
340
341        let rule = AsyncUniqueRule::new("users", "email");
342        let err = rule
343            .validate_async("test@example.com", &ctx)
344            .await
345            .unwrap_err();
346        assert!(err.message.contains("not configured"));
347    }
348
349    #[test]
350    fn async_rule_serialization() {
351        let rule = AsyncUniqueRule::new("users", "email").with_message("Email already taken");
352        let json = serde_json::to_string(&rule).unwrap();
353        let parsed: AsyncUniqueRule = serde_json::from_str(&json).unwrap();
354        assert_eq!(rule, parsed);
355    }
356}