rustapi_validate/v2/
context.rs

1//! Validation context for async operations.
2
3use async_trait::async_trait;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7/// Trait for database validation operations.
8#[async_trait]
9pub trait DatabaseValidator: Send + Sync {
10    /// Check if a value exists in a table column.
11    async fn exists(&self, table: &str, column: &str, value: &str) -> Result<bool, String>;
12
13    /// Check if a value is unique in a table column.
14    async fn is_unique(&self, table: &str, column: &str, value: &str) -> Result<bool, String>;
15
16    /// Check if a value is unique, excluding a specific ID (for updates).
17    async fn is_unique_except(
18        &self,
19        table: &str,
20        column: &str,
21        value: &str,
22        except_id: &str,
23    ) -> Result<bool, String>;
24}
25
26/// Trait for HTTP/API validation operations.
27#[async_trait]
28pub trait HttpValidator: Send + Sync {
29    /// Validate a value against an external API endpoint.
30    async fn validate(&self, endpoint: &str, value: &str) -> Result<bool, String>;
31}
32
33/// Trait for custom async validators.
34#[async_trait]
35pub trait CustomValidator: Send + Sync {
36    /// Validate a value with custom logic.
37    async fn validate(&self, value: &str) -> Result<bool, String>;
38}
39
40/// Context for async validation operations.
41///
42/// Provides access to database, HTTP, and custom validators for async validation rules.
43///
44/// ## Example
45///
46/// ```rust,ignore
47/// use rustapi_validate::v2::prelude::*;
48///
49/// let ctx = ValidationContextBuilder::new()
50///     .database(my_db_validator)
51///     .http(my_http_client)
52///     .build();
53///
54/// user.validate_async(&ctx).await?;
55/// ```
56#[derive(Default)]
57pub struct ValidationContext {
58    database: Option<Arc<dyn DatabaseValidator>>,
59    http: Option<Arc<dyn HttpValidator>>,
60    custom: HashMap<String, Arc<dyn CustomValidator>>,
61    /// ID to exclude from uniqueness checks (for updates)
62    exclude_id: Option<String>,
63}
64
65impl ValidationContext {
66    /// Create a new empty validation context.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Get the database validator if configured.
72    pub fn database(&self) -> Option<&Arc<dyn DatabaseValidator>> {
73        self.database.as_ref()
74    }
75
76    /// Get the HTTP validator if configured.
77    pub fn http(&self) -> Option<&Arc<dyn HttpValidator>> {
78        self.http.as_ref()
79    }
80
81    /// Get a custom validator by name.
82    pub fn custom(&self, name: &str) -> Option<&Arc<dyn CustomValidator>> {
83        self.custom.get(name)
84    }
85
86    /// Get the ID to exclude from uniqueness checks.
87    pub fn exclude_id(&self) -> Option<&str> {
88        self.exclude_id.as_deref()
89    }
90
91    /// Create a builder for constructing a validation context.
92    pub fn builder() -> ValidationContextBuilder {
93        ValidationContextBuilder::new()
94    }
95}
96
97impl std::fmt::Debug for ValidationContext {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("ValidationContext")
100            .field("has_database", &self.database.is_some())
101            .field("has_http", &self.http.is_some())
102            .field("custom_validators", &self.custom.keys().collect::<Vec<_>>())
103            .field("exclude_id", &self.exclude_id)
104            .finish()
105    }
106}
107
108/// Builder for constructing a `ValidationContext`.
109#[derive(Default)]
110pub struct ValidationContextBuilder {
111    database: Option<Arc<dyn DatabaseValidator>>,
112    http: Option<Arc<dyn HttpValidator>>,
113    custom: HashMap<String, Arc<dyn CustomValidator>>,
114    exclude_id: Option<String>,
115}
116
117impl ValidationContextBuilder {
118    /// Create a new builder.
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    /// Set the database validator.
124    pub fn database(mut self, validator: impl DatabaseValidator + 'static) -> Self {
125        self.database = Some(Arc::new(validator));
126        self
127    }
128
129    /// Set the database validator from an Arc.
130    pub fn database_arc(mut self, validator: Arc<dyn DatabaseValidator>) -> Self {
131        self.database = Some(validator);
132        self
133    }
134
135    /// Set the HTTP validator.
136    pub fn http(mut self, validator: impl HttpValidator + 'static) -> Self {
137        self.http = Some(Arc::new(validator));
138        self
139    }
140
141    /// Set the HTTP validator from an Arc.
142    pub fn http_arc(mut self, validator: Arc<dyn HttpValidator>) -> Self {
143        self.http = Some(validator);
144        self
145    }
146
147    /// Add a custom validator.
148    pub fn custom(
149        mut self,
150        name: impl Into<String>,
151        validator: impl CustomValidator + 'static,
152    ) -> Self {
153        self.custom.insert(name.into(), Arc::new(validator));
154        self
155    }
156
157    /// Add a custom validator from an Arc.
158    pub fn custom_arc(
159        mut self,
160        name: impl Into<String>,
161        validator: Arc<dyn CustomValidator>,
162    ) -> Self {
163        self.custom.insert(name.into(), validator);
164        self
165    }
166
167    /// Set the ID to exclude from uniqueness checks (for updates).
168    pub fn exclude_id(mut self, id: impl Into<String>) -> Self {
169        self.exclude_id = Some(id.into());
170        self
171    }
172
173    /// Build the validation context.
174    pub fn build(self) -> ValidationContext {
175        ValidationContext {
176            database: self.database,
177            http: self.http,
178            custom: self.custom,
179            exclude_id: self.exclude_id,
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    struct MockDbValidator;
189
190    #[async_trait]
191    impl DatabaseValidator for MockDbValidator {
192        async fn exists(&self, _table: &str, _column: &str, _value: &str) -> Result<bool, String> {
193            Ok(true)
194        }
195
196        async fn is_unique(
197            &self,
198            _table: &str,
199            _column: &str,
200            _value: &str,
201        ) -> Result<bool, String> {
202            Ok(true)
203        }
204
205        async fn is_unique_except(
206            &self,
207            _table: &str,
208            _column: &str,
209            _value: &str,
210            _except_id: &str,
211        ) -> Result<bool, String> {
212            Ok(true)
213        }
214    }
215
216    #[test]
217    fn context_builder() {
218        let ctx = ValidationContextBuilder::new()
219            .database(MockDbValidator)
220            .exclude_id("123")
221            .build();
222
223        assert!(ctx.database().is_some());
224        assert!(ctx.http().is_none());
225        assert_eq!(ctx.exclude_id(), Some("123"));
226    }
227
228    #[test]
229    fn empty_context() {
230        let ctx = ValidationContext::new();
231        assert!(ctx.database().is_none());
232        assert!(ctx.http().is_none());
233        assert!(ctx.exclude_id().is_none());
234    }
235}