Skip to main content

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    /// Locale for error messages (e.g. "en", "tr")
64    locale: Option<String>,
65}
66
67impl ValidationContext {
68    /// Create a new empty validation context.
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Get the database validator if configured.
74    pub fn database(&self) -> Option<&Arc<dyn DatabaseValidator>> {
75        self.database.as_ref()
76    }
77
78    /// Get the HTTP validator if configured.
79    pub fn http(&self) -> Option<&Arc<dyn HttpValidator>> {
80        self.http.as_ref()
81    }
82
83    /// Get a custom validator by name.
84    pub fn custom(&self, name: &str) -> Option<&Arc<dyn CustomValidator>> {
85        self.custom.get(name)
86    }
87
88    /// Get the locale.
89    pub fn locale(&self) -> Option<&str> {
90        self.locale.as_deref()
91    }
92
93    /// Get the ID to exclude from uniqueness checks.
94    pub fn exclude_id(&self) -> Option<&str> {
95        self.exclude_id.as_deref()
96    }
97
98    /// Create a builder for constructing a validation context.
99    pub fn builder() -> ValidationContextBuilder {
100        ValidationContextBuilder::new()
101    }
102}
103
104impl std::fmt::Debug for ValidationContext {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("ValidationContext")
107            .field("has_database", &self.database.is_some())
108            .field("has_http", &self.http.is_some())
109            .field("custom_validators", &self.custom.keys().collect::<Vec<_>>())
110            .field("exclude_id", &self.exclude_id)
111            .field("locale", &self.locale)
112            .finish()
113    }
114}
115
116/// Builder for constructing a `ValidationContext`.
117#[derive(Default)]
118pub struct ValidationContextBuilder {
119    database: Option<Arc<dyn DatabaseValidator>>,
120    http: Option<Arc<dyn HttpValidator>>,
121    custom: HashMap<String, Arc<dyn CustomValidator>>,
122    exclude_id: Option<String>,
123    locale: Option<String>,
124}
125
126impl ValidationContextBuilder {
127    /// Create a new builder.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Set the database validator.
133    pub fn database(mut self, validator: impl DatabaseValidator + 'static) -> Self {
134        self.database = Some(Arc::new(validator));
135        self
136    }
137
138    /// Set the database validator from an Arc.
139    pub fn database_arc(mut self, validator: Arc<dyn DatabaseValidator>) -> Self {
140        self.database = Some(validator);
141        self
142    }
143
144    /// Set the HTTP validator.
145    pub fn http(mut self, validator: impl HttpValidator + 'static) -> Self {
146        self.http = Some(Arc::new(validator));
147        self
148    }
149
150    /// Set the HTTP validator from an Arc.
151    pub fn http_arc(mut self, validator: Arc<dyn HttpValidator>) -> Self {
152        self.http = Some(validator);
153        self
154    }
155
156    /// Add a custom validator.
157    pub fn custom(
158        mut self,
159        name: impl Into<String>,
160        validator: impl CustomValidator + 'static,
161    ) -> Self {
162        self.custom.insert(name.into(), Arc::new(validator));
163        self
164    }
165
166    /// Add a custom validator from an Arc.
167    pub fn custom_arc(
168        mut self,
169        name: impl Into<String>,
170        validator: Arc<dyn CustomValidator>,
171    ) -> Self {
172        self.custom.insert(name.into(), validator);
173        self
174    }
175
176    /// Set the ID to exclude from uniqueness checks (for updates).
177    pub fn exclude_id(mut self, id: impl Into<String>) -> Self {
178        self.exclude_id = Some(id.into());
179        self
180    }
181
182    /// Set the locale.
183    pub fn locale(mut self, locale: impl Into<String>) -> Self {
184        self.locale = Some(locale.into());
185        self
186    }
187
188    /// Build the validation context.
189    pub fn build(self) -> ValidationContext {
190        ValidationContext {
191            database: self.database,
192            http: self.http,
193            custom: self.custom,
194            exclude_id: self.exclude_id,
195            locale: self.locale,
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    struct MockDbValidator;
205
206    #[async_trait]
207    impl DatabaseValidator for MockDbValidator {
208        async fn exists(&self, _table: &str, _column: &str, _value: &str) -> Result<bool, String> {
209            Ok(true)
210        }
211
212        async fn is_unique(
213            &self,
214            _table: &str,
215            _column: &str,
216            _value: &str,
217        ) -> Result<bool, String> {
218            Ok(true)
219        }
220
221        async fn is_unique_except(
222            &self,
223            _table: &str,
224            _column: &str,
225            _value: &str,
226            _except_id: &str,
227        ) -> Result<bool, String> {
228            Ok(true)
229        }
230    }
231
232    #[test]
233    fn context_builder() {
234        let ctx = ValidationContextBuilder::new()
235            .database(MockDbValidator)
236            .exclude_id("123")
237            .build();
238
239        assert!(ctx.database().is_some());
240        assert!(ctx.http().is_none());
241        assert_eq!(ctx.exclude_id(), Some("123"));
242    }
243
244    #[test]
245    fn empty_context() {
246        let ctx = ValidationContext::new();
247        assert!(ctx.database().is_none());
248        assert!(ctx.http().is_none());
249        assert!(ctx.exclude_id().is_none());
250    }
251}