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}