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.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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98pub struct AsyncExistsRule {
99 pub table: String,
101 pub column: String,
103 #[serde(skip_serializing_if = "Option::is_none")]
105 pub message: Option<String>,
106}
107
108impl AsyncExistsRule {
109 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
176pub struct AsyncApiRule {
177 pub endpoint: String,
179 #[serde(skip_serializing_if = "Option::is_none")]
181 pub message: Option<String>,
182}
183
184impl AsyncApiRule {
185 pub fn new(endpoint: impl Into<String>) -> Self {
187 Self {
188 endpoint: endpoint.into(),
189 message: None,
190 }
191 }
192
193 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}