axum_gate/permissions/
collision_checker.rs

1use super::{PermissionCollision, PermissionId, ValidationReport};
2use crate::errors::{Error, Result};
3use crate::permissions::PermissionsError;
4use std::collections::HashMap;
5
6/// Low-level permission collision checker for runtime validation and analysis.
7///
8/// This checker validates permission strings for duplicates and hash collisions,
9/// providing detailed reports about any issues found. Unlike the compile-time
10/// validation, this can handle dynamic permission strings.
11///
12/// ## Use Cases
13///
14/// - **Runtime validation**: When permissions change during application lifecycle
15/// - **Debugging and analysis**: Need to inspect collision maps and conflicts
16/// - **Custom validation workflows**: Require fine-grained control over validation process
17/// - **Performance-critical paths**: Direct validation without builder overhead
18///
19/// ## Compared to ApplicationValidator
20///
21/// - **State**: Stateful - maintains collision map for post-validation analysis
22/// - **Usage**: Direct instantiation with complete permission set
23/// - **Methods**: Provides introspection methods like `get_conflicting_permissions()`
24/// - **Lifecycle**: Can be reused after validation for analysis
25///
26/// For simple application startup validation, consider using [`ApplicationValidator`](super::ApplicationValidator)
27/// which provides a more ergonomic builder pattern API.
28///
29/// # See Also
30///
31/// - [`ApplicationValidator`](super::ApplicationValidator) - High-level builder pattern validator for startup validation
32///
33/// # Examples
34///
35/// ## Basic validation with post-analysis
36///
37/// ```
38/// use axum_gate::permissions::PermissionCollisionChecker;
39///
40/// let permissions = vec![
41///     "user:read".to_string(),
42///     "user:write".to_string(),
43///     "admin:full_access".to_string(),
44/// ];
45///
46/// let mut checker = PermissionCollisionChecker::new(permissions);
47/// let report = checker.validate()?;
48///
49/// if report.is_valid() {
50///     println!("All permissions are valid!");
51///     // Can still use checker for analysis after validation
52///     println!("Total permissions: {}", checker.permission_count());
53///     println!("Unique IDs: {}", checker.unique_id_count());
54/// } else {
55///     println!("Issues found: {}", report.summary());
56///     // Check for specific conflicts
57///     let conflicts = checker.get_conflicting_permissions("user:read");
58///     if !conflicts.is_empty() {
59///         println!("Conflicts with user:read: {:?}", conflicts);
60///     }
61/// }
62/// # Ok::<(), axum_gate::errors::Error>(())
63/// ```
64///
65/// ## Runtime permission updates
66///
67/// ```
68/// use axum_gate::permissions::PermissionCollisionChecker;
69///
70/// fn update_permissions(new_permissions: Vec<String>) -> Result<(), Box<dyn std::error::Error>> {
71///     let mut checker = PermissionCollisionChecker::new(new_permissions);
72///     let report = checker.validate()?;
73///
74///     if !report.is_valid() {
75///         // Can analyze specific issues
76///         for collision in &report.collisions {
77///             println!("Hash ID {} has conflicts: {:?}", collision.id, collision.permissions);
78///         }
79///         return Err("Permission validation failed".into());
80///     }
81///
82///     // Validation passed - can still inspect the checker
83///     let summary = checker.get_permission_summary();
84///     println!("Permission distribution: {:?}", summary);
85///     Ok(())
86/// }
87/// ```
88pub struct PermissionCollisionChecker {
89    permissions: Vec<String>,
90    collision_map: HashMap<u64, Vec<String>>,
91}
92
93impl PermissionCollisionChecker {
94    /// Creates a new collision checker with the given permission strings.
95    ///
96    /// # Arguments
97    ///
98    /// * `permissions` - Vector of permission strings to validate
99    pub fn new(permissions: Vec<String>) -> Self {
100        Self {
101            permissions,
102            collision_map: HashMap::new(),
103        }
104    }
105
106    /// Validates all permissions for uniqueness and collision-free hashing.
107    ///
108    /// This method performs comprehensive validation including:
109    /// - Duplicate string detection
110    /// - Hash collision detection
111    /// - Internal collision map building
112    ///
113    /// # Returns
114    ///
115    /// * `Ok(ValidationReport)` - Detailed report of validation results
116    /// * `Err(axum_gate::errors::Error)` - If validation process itself fails
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use axum_gate::permissions::PermissionCollisionChecker;
122    ///
123    /// let permissions = vec!["read:file".to_string(), "write:file".to_string()];
124    /// let mut checker = PermissionCollisionChecker::new(permissions);
125    ///
126    /// match checker.validate() {
127    ///     Ok(report) => {
128    ///         if report.is_valid() {
129    ///             println!("Validation passed!");
130    ///         } else {
131    ///             eprintln!("Validation failed: {}", report.summary());
132    ///         }
133    ///     }
134    ///     Err(e) => eprintln!("Validation error: {}", e),
135    /// }
136    /// ```
137    pub fn validate(&mut self) -> Result<ValidationReport> {
138        let mut report = ValidationReport::default();
139
140        // Check for hash collisions (including duplicates)
141        self.check_hash_collisions(&mut report).map_err(|e| {
142            Error::Permissions(PermissionsError::collision(
143                0,
144                vec![format!("Failed to check for hash collisions: {}", e)],
145            ))
146        })?;
147
148        // Generate collision map for inspection
149        self.build_collision_map();
150
151        Ok(report)
152    }
153
154    fn check_hash_collisions(&self, report: &mut ValidationReport) -> Result<()> {
155        let mut id_to_permissions: HashMap<u64, Vec<String>> = HashMap::new();
156
157        // Group permissions by their hash ID
158        for permission in &self.permissions {
159            let id_raw = PermissionId::from(permission.as_str()).as_u64();
160            id_to_permissions
161                .entry(id_raw)
162                .or_default()
163                .push(permission.clone());
164        }
165
166        // Find all hash IDs with multiple permissions
167        for (id, permissions) in id_to_permissions {
168            if permissions.len() > 1 {
169                report
170                    .collisions
171                    .push(PermissionCollision { id, permissions });
172            }
173        }
174
175        Ok(())
176    }
177
178    fn build_collision_map(&mut self) {
179        self.collision_map.clear();
180
181        for permission in &self.permissions {
182            let id = PermissionId::from(permission.as_str()).as_u64();
183            self.collision_map
184                .entry(id)
185                .or_default()
186                .push(permission.clone());
187        }
188    }
189
190    /// Returns permissions that hash to the same ID as the given permission.
191    ///
192    /// This method is useful for debugging collision issues or understanding
193    /// how permissions map to hash IDs.
194    ///
195    /// # Arguments
196    ///
197    /// * `permission` - The permission string to check for conflicts
198    ///
199    /// # Returns
200    ///
201    /// Vector of permission strings that conflict with the given permission.
202    /// The returned vector will not include the input permission itself.
203    pub fn get_conflicting_permissions(&self, permission: &str) -> Vec<String> {
204        let id = PermissionId::from(permission).as_u64();
205        self.collision_map
206            .get(&id)
207            .map(|perms| perms.iter().filter(|p| *p != permission).cloned().collect())
208            .unwrap_or_default()
209    }
210
211    /// Returns a summary of all permissions grouped by their hash ID.
212    ///
213    /// This method provides a complete view of how permissions are distributed
214    /// across hash IDs, which can be useful for analysis and debugging.
215    ///
216    /// # Returns
217    ///
218    /// HashMap where keys are hash IDs and values are vectors of permission strings
219    /// that hash to that ID.
220    pub fn get_permission_summary(&self) -> HashMap<u64, Vec<String>> {
221        self.collision_map.clone()
222    }
223
224    /// Returns the total number of permissions being validated.
225    pub fn permission_count(&self) -> usize {
226        self.permissions.len()
227    }
228
229    /// Returns the number of unique hash IDs generated from the permissions.
230    pub fn unique_id_count(&self) -> usize {
231        self.collision_map.len()
232    }
233}
234
235#[cfg(test)]
236#[allow(clippy::unwrap_used)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn collision_checker_valid_permissions() {
242        let permissions = vec![
243            "user:read".to_string(),
244            "user:write".to_string(),
245            "admin:delete".to_string(),
246        ];
247
248        let mut checker = PermissionCollisionChecker::new(permissions);
249        let report = checker.validate().unwrap();
250
251        assert!(report.is_valid());
252        assert!(report.duplicates().is_empty());
253        assert!(report.collisions.is_empty());
254    }
255
256    #[test]
257    fn collision_checker_duplicate_strings() {
258        let permissions = vec![
259            "user:read".to_string(),
260            "user:write".to_string(),
261            "user:read".to_string(), // Duplicate
262        ];
263
264        let mut checker = PermissionCollisionChecker::new(permissions);
265        let report = checker.validate().unwrap();
266
267        assert!(!report.is_valid());
268        let duplicates = report.duplicates();
269        assert_eq!(duplicates.len(), 1);
270        assert_eq!(duplicates[0], "user:read");
271        assert_eq!(report.collisions.len(), 1);
272    }
273
274    #[test]
275    fn collision_checker_conflicting_permissions() {
276        let permissions = vec!["user:read".to_string(), "user:write".to_string()];
277
278        let mut checker = PermissionCollisionChecker::new(permissions);
279        checker.validate().unwrap();
280
281        let conflicts = checker.get_conflicting_permissions("user:read");
282        // Since these shouldn't hash to the same value, conflicts should be empty
283        assert!(conflicts.is_empty());
284    }
285
286    #[test]
287    fn permission_collision_checker_summary() {
288        let permissions = vec![
289            "user:read".to_string(),
290            "user:write".to_string(),
291            "admin:delete".to_string(),
292        ];
293
294        let mut checker = PermissionCollisionChecker::new(permissions);
295        checker.validate().unwrap();
296
297        assert_eq!(checker.permission_count(), 3);
298        // Should have 3 unique IDs (assuming no collisions)
299        assert_eq!(checker.unique_id_count(), 3);
300
301        let summary = checker.get_permission_summary();
302        assert_eq!(summary.len(), 3);
303    }
304}