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