axum_gate/permissions/
application_validator.rs

1use super::{PermissionCollisionChecker, ValidationReport};
2use crate::errors::{Error, Result};
3use crate::permissions::PermissionsError;
4use tracing::info;
5
6/// High-level builder pattern validator for application startup validation.
7///
8/// This is a high-level interface for validating permissions from multiple sources
9/// during application initialization. It provides an ergonomic API for collecting
10/// permissions incrementally before validation.
11///
12/// ## Use Cases
13///
14/// - **Application startup**: Validate permissions loaded from config, database, etc.
15/// - **Simple validation workflows**: Need basic validation with automatic logging
16/// - **Builder pattern preference**: Want to incrementally add permissions from different sources
17/// - **One-time validation**: Don't need post-validation analysis
18///
19/// ## Compared to PermissionCollisionChecker
20///
21/// - **State**: Stateless builder - consumed during validation
22/// - **Usage**: Builder pattern with incremental permission addition
23/// - **Methods**: Focus on building and validating, no post-validation introspection
24/// - **Lifecycle**: Single-use - transforms into validation report
25/// - **Logging**: Automatically logs validation results
26///
27/// For runtime validation or when you need to analyze collision details after validation,
28/// use [`PermissionCollisionChecker`] directly.
29///
30/// # See Also
31///
32/// - [`PermissionCollisionChecker`] - Low-level validator with detailed analysis capabilities
33///
34/// # Examples
35///
36/// ## Application startup validation
37///
38/// ```
39/// use axum_gate::permissions::ApplicationValidator;
40///
41/// # fn load_config_permissions() -> Vec<String> { vec!["user:read".to_string()] }
42/// # async fn load_db_permissions() -> Result<Vec<String>, Box<dyn std::error::Error>> { Ok(vec!["admin:write".to_string()]) }
43/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
44/// // Collect permissions from multiple sources during startup
45/// let config_permissions = load_config_permissions();
46/// let db_permissions = load_db_permissions().await?;
47///
48/// let report = ApplicationValidator::new()
49///     .add_permissions(config_permissions)
50///     .add_permissions(db_permissions)
51///     .add_permission("system:health")  // Add individual permissions
52///     .validate()?;  // Automatically logs results
53///
54/// if report.is_valid() {
55///     println!("✓ All permissions validated - server can start");
56/// } else {
57///     return Err(format!("Permission validation failed: {}", report.summary()).into());
58/// }
59/// # Ok(())
60/// # }
61/// ```
62///
63/// ## Simple validation workflow
64///
65/// ```
66/// use axum_gate::permissions::ApplicationValidator;
67///
68/// // For simple cases where you just need pass/fail validation
69/// let report = ApplicationValidator::new()
70///     .add_permissions(["user:read", "user:write", "admin:delete"])
71///     .validate()?;
72///
73/// // Report is automatically logged, just check if valid
74/// if !report.is_valid() {
75///     panic!("Invalid permissions detected during startup");
76/// }
77/// # Ok::<(), axum_gate::errors::Error>(())
78/// ```
79///
80/// ## Comparison with PermissionCollisionChecker
81///
82/// ```
83/// use axum_gate::permissions::{ApplicationValidator, PermissionCollisionChecker};
84///
85/// let permissions = vec!["user:read".to_string(), "user:write".to_string()];
86///
87/// // ApplicationValidator: Builder pattern, single-use, automatic logging
88/// let report1 = ApplicationValidator::new()
89///     .add_permissions(permissions.clone())
90///     .validate()?;  // Validator is consumed here
91/// // Can't use validator anymore, but don't need to
92///
93/// // PermissionCollisionChecker: Direct instantiation, reusable, manual control
94/// let mut checker = PermissionCollisionChecker::new(permissions);
95/// let report2 = checker.validate()?;  // Checker is still available
96///
97/// // Can continue using checker for analysis
98/// if !report2.is_valid() {
99///     let conflicts = checker.get_conflicting_permissions("user:read");
100///     println!("Conflicts found: {:?}", conflicts);
101/// }
102/// # Ok::<(), axum_gate::errors::Error>(())
103/// ```
104pub struct ApplicationValidator {
105    permissions: Vec<String>,
106}
107
108impl ApplicationValidator {
109    /// Creates a new application validator.
110    pub fn new() -> Self {
111        Self {
112            permissions: Vec::new(),
113        }
114    }
115
116    /// Add permissions from an iterator of string-like types.
117    ///
118    /// # Arguments
119    ///
120    /// * `permissions` - Iterator of items that can be converted to String
121    pub fn add_permissions<I, S>(mut self, permissions: I) -> Self
122    where
123        I: IntoIterator<Item = S>,
124        S: Into<String>,
125    {
126        self.permissions
127            .extend(permissions.into_iter().map(|s| s.into()));
128        self
129    }
130
131    /// Add permissions from a vector of strings.
132    ///
133    /// This is a convenience method for adding permissions that are already
134    /// in String format.
135    ///
136    /// # Arguments
137    ///
138    /// * `permissions` - Vector of permission strings
139    pub fn add_permission_strings(mut self, permissions: Vec<String>) -> Self {
140        self.permissions.extend(permissions);
141        self
142    }
143
144    /// Add a single permission string.
145    ///
146    /// # Arguments
147    ///
148    /// * `permission` - A single permission string to add
149    pub fn add_permission<S: Into<String>>(mut self, permission: S) -> Self {
150        self.permissions.push(permission.into());
151        self
152    }
153
154    /// Validate all permissions and return detailed report.
155    ///
156    /// This method performs validation and logs results automatically.
157    /// It returns a ValidationReport containing all validation details,
158    /// regardless of whether validation passed or failed.
159    ///
160    /// # Returns
161    ///
162    /// * `Ok(ValidationReport)` - Complete validation report
163    /// * `Err(axum_gate::errors::Error)` - Validation process failed
164    pub fn validate(self) -> Result<ValidationReport> {
165        let mut checker = PermissionCollisionChecker::new(self.permissions);
166        let report = checker.validate().map_err(|e| {
167            Error::Permissions(PermissionsError::collision(
168                0,
169                vec![format!("Permission validation process failed: {}", e)],
170            ))
171        })?;
172
173        report.log_results();
174
175        if report.is_valid() {
176            info!("✓ Permission validation completed successfully");
177        }
178
179        Ok(report)
180    }
181
182    /// Returns the current number of permissions to be validated.
183    pub fn permission_count(&self) -> usize {
184        self.permissions.len()
185    }
186}
187
188impl Default for ApplicationValidator {
189    fn default() -> Self {
190        Self::new()
191    }
192}
193
194#[cfg(test)]
195#[allow(clippy::unwrap_used)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn application_validator_basic() {
201        let result = ApplicationValidator::new()
202            .add_permissions(["user:read", "user:write"])
203            .add_permission("admin:delete")
204            .validate();
205
206        assert!(result.is_ok());
207        let report = result.unwrap();
208        assert!(report.is_valid());
209    }
210
211    #[test]
212    fn application_validator_with_duplicates() {
213        let result = ApplicationValidator::new()
214            .add_permissions(["user:read", "user:read"])
215            .validate();
216
217        assert!(result.is_ok());
218        let report = result.unwrap();
219        assert!(!report.is_valid());
220        assert!(!report.duplicates().is_empty());
221    }
222}