rustapi_validate/v2/rules/
async_rules.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct AsyncUniqueRule {
16 pub table: String,
18 pub column: String,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub message: Option<String>,
23}
24
25impl AsyncUniqueRule {
26 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99pub struct AsyncExistsRule {
100 pub table: String,
102 pub column: String,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub message: Option<String>,
107}
108
109impl AsyncExistsRule {
110 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178pub struct AsyncApiRule {
179 pub endpoint: String,
181 #[serde(skip_serializing_if = "Option::is_none")]
183 pub message: Option<String>,
184}
185
186impl AsyncApiRule {
187 pub fn new(endpoint: impl Into<String>) -> Self {
189 Self {
190 endpoint: endpoint.into(),
191 message: None,
192 }
193 }
194
195 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}