scim_server/storage/
errors.rs

1//! Storage-specific error types for pure data operations.
2//!
3//! This module defines errors that can occur during storage operations, separate from
4//! SCIM protocol errors or business logic errors. These errors focus on data persistence
5//! and retrieval failures.
6
7use std::fmt;
8
9/// Errors that can occur during storage operations.
10///
11/// These errors represent failures in the storage layer and are protocol-agnostic.
12/// They focus on data persistence, retrieval, and basic storage operations without
13/// any knowledge of SCIM semantics or business rules.
14#[derive(Debug)]
15pub enum StorageError {
16    /// The requested resource was not found.
17    ResourceNotFound {
18        tenant_id: String,
19        resource_type: String,
20        id: String,
21    },
22
23    /// The resource already exists when it shouldn't (for operations requiring uniqueness).
24    ResourceAlreadyExists {
25        tenant_id: String,
26        resource_type: String,
27        id: String,
28    },
29
30    /// Invalid data format or structure that cannot be stored.
31    InvalidData {
32        message: String,
33        cause: Option<String>,
34    },
35
36    /// The tenant was not found or is invalid.
37    TenantNotFound { tenant_id: String },
38
39    /// Invalid query parameters or search criteria.
40    InvalidQuery {
41        message: String,
42        attribute: Option<String>,
43        value: Option<String>,
44    },
45
46    /// Storage capacity exceeded (disk full, memory limit, etc.).
47    CapacityExceeded {
48        message: String,
49        current_count: Option<usize>,
50        limit: Option<usize>,
51    },
52
53    /// Concurrent modification detected (optimistic locking failure).
54    ConcurrentModification {
55        tenant_id: String,
56        resource_type: String,
57        id: String,
58        expected_version: Option<String>,
59        actual_version: Option<String>,
60    },
61
62    /// Storage backend is temporarily unavailable.
63    Unavailable {
64        message: String,
65        retry_after: Option<std::time::Duration>,
66    },
67
68    /// Permission denied for the storage operation.
69    PermissionDenied { operation: String, resource: String },
70
71    /// Timeout occurred during storage operation.
72    Timeout {
73        operation: String,
74        duration: std::time::Duration,
75    },
76
77    /// Corruption detected in stored data.
78    DataCorruption {
79        tenant_id: String,
80        resource_type: String,
81        id: Option<String>,
82        details: String,
83    },
84
85    /// Configuration error in the storage backend.
86    Configuration {
87        message: String,
88        parameter: Option<String>,
89    },
90
91    /// Network-related error for distributed storage systems.
92    Network {
93        message: String,
94        endpoint: Option<String>,
95    },
96
97    /// Serialization or deserialization error.
98    Serialization {
99        message: String,
100        data_type: Option<String>,
101    },
102
103    /// Generic internal storage error.
104    Internal {
105        message: String,
106        source: Option<Box<dyn std::error::Error + Send + Sync>>,
107    },
108}
109
110impl fmt::Display for StorageError {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            StorageError::ResourceNotFound {
114                tenant_id,
115                resource_type,
116                id,
117            } => {
118                write!(
119                    f,
120                    "Resource not found: {}/{}/{}",
121                    tenant_id, resource_type, id
122                )
123            }
124            StorageError::ResourceAlreadyExists {
125                tenant_id,
126                resource_type,
127                id,
128            } => {
129                write!(
130                    f,
131                    "Resource already exists: {}/{}/{}",
132                    tenant_id, resource_type, id
133                )
134            }
135            StorageError::InvalidData { message, cause } => {
136                if let Some(cause) = cause {
137                    write!(f, "Invalid data: {} (cause: {})", message, cause)
138                } else {
139                    write!(f, "Invalid data: {}", message)
140                }
141            }
142            StorageError::TenantNotFound { tenant_id } => {
143                write!(f, "Tenant not found: {}", tenant_id)
144            }
145            StorageError::InvalidQuery {
146                message,
147                attribute,
148                value,
149            } => match (attribute, value) {
150                (Some(attr), Some(val)) => {
151                    write!(
152                        f,
153                        "Invalid query: {} (attribute: {}, value: {})",
154                        message, attr, val
155                    )
156                }
157                (Some(attr), None) => {
158                    write!(f, "Invalid query: {} (attribute: {})", message, attr)
159                }
160                _ => write!(f, "Invalid query: {}", message),
161            },
162            StorageError::CapacityExceeded {
163                message,
164                current_count,
165                limit,
166            } => match (current_count, limit) {
167                (Some(current), Some(max)) => {
168                    write!(f, "Capacity exceeded: {} ({}/{})", message, current, max)
169                }
170                _ => write!(f, "Capacity exceeded: {}", message),
171            },
172            StorageError::ConcurrentModification {
173                tenant_id,
174                resource_type,
175                id,
176                expected_version,
177                actual_version,
178            } => match (expected_version, actual_version) {
179                (Some(expected), Some(actual)) => {
180                    write!(
181                        f,
182                        "Concurrent modification detected for {}/{}/{}: expected version {}, found {}",
183                        tenant_id, resource_type, id, expected, actual
184                    )
185                }
186                _ => {
187                    write!(
188                        f,
189                        "Concurrent modification detected for {}/{}/{}",
190                        tenant_id, resource_type, id
191                    )
192                }
193            },
194            StorageError::Unavailable {
195                message,
196                retry_after,
197            } => {
198                if let Some(duration) = retry_after {
199                    write!(
200                        f,
201                        "Storage unavailable: {} (retry after {:?})",
202                        message, duration
203                    )
204                } else {
205                    write!(f, "Storage unavailable: {}", message)
206                }
207            }
208            StorageError::PermissionDenied {
209                operation,
210                resource,
211            } => {
212                write!(f, "Permission denied: {} on {}", operation, resource)
213            }
214            StorageError::Timeout {
215                operation,
216                duration,
217            } => {
218                write!(f, "Timeout during {} after {:?}", operation, duration)
219            }
220            StorageError::DataCorruption {
221                tenant_id,
222                resource_type,
223                id,
224                details,
225            } => {
226                if let Some(resource_id) = id {
227                    write!(
228                        f,
229                        "Data corruption in {}/{}/{}: {}",
230                        tenant_id, resource_type, resource_id, details
231                    )
232                } else {
233                    write!(
234                        f,
235                        "Data corruption in {}/{}: {}",
236                        tenant_id, resource_type, details
237                    )
238                }
239            }
240            StorageError::Configuration { message, parameter } => {
241                if let Some(param) = parameter {
242                    write!(f, "Configuration error: {} (parameter: {})", message, param)
243                } else {
244                    write!(f, "Configuration error: {}", message)
245                }
246            }
247            StorageError::Network { message, endpoint } => {
248                if let Some(ep) = endpoint {
249                    write!(f, "Network error: {} (endpoint: {})", message, ep)
250                } else {
251                    write!(f, "Network error: {}", message)
252                }
253            }
254            StorageError::Serialization { message, data_type } => {
255                if let Some(dtype) = data_type {
256                    write!(f, "Serialization error: {} (type: {})", message, dtype)
257                } else {
258                    write!(f, "Serialization error: {}", message)
259                }
260            }
261            StorageError::Internal { message, .. } => {
262                write!(f, "Internal storage error: {}", message)
263            }
264        }
265    }
266}
267
268impl std::error::Error for StorageError {
269    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
270        match self {
271            StorageError::Internal { source, .. } => source
272                .as_ref()
273                .map(|e| e.as_ref() as &(dyn std::error::Error + 'static)),
274            _ => None,
275        }
276    }
277}
278
279impl StorageError {
280    /// Create a new ResourceNotFound error.
281    pub fn resource_not_found(
282        tenant_id: impl Into<String>,
283        resource_type: impl Into<String>,
284        id: impl Into<String>,
285    ) -> Self {
286        Self::ResourceNotFound {
287            tenant_id: tenant_id.into(),
288            resource_type: resource_type.into(),
289            id: id.into(),
290        }
291    }
292
293    /// Create a new ResourceAlreadyExists error.
294    pub fn resource_already_exists(
295        tenant_id: impl Into<String>,
296        resource_type: impl Into<String>,
297        id: impl Into<String>,
298    ) -> Self {
299        Self::ResourceAlreadyExists {
300            tenant_id: tenant_id.into(),
301            resource_type: resource_type.into(),
302            id: id.into(),
303        }
304    }
305
306    /// Create a new InvalidData error.
307    pub fn invalid_data(message: impl Into<String>) -> Self {
308        Self::InvalidData {
309            message: message.into(),
310            cause: None,
311        }
312    }
313
314    /// Create a new InvalidData error with a cause.
315    pub fn invalid_data_with_cause(message: impl Into<String>, cause: impl Into<String>) -> Self {
316        Self::InvalidData {
317            message: message.into(),
318            cause: Some(cause.into()),
319        }
320    }
321
322    /// Create a new TenantNotFound error.
323    pub fn tenant_not_found(tenant_id: impl Into<String>) -> Self {
324        Self::TenantNotFound {
325            tenant_id: tenant_id.into(),
326        }
327    }
328
329    /// Create a new InvalidQuery error.
330    pub fn invalid_query(message: impl Into<String>) -> Self {
331        Self::InvalidQuery {
332            message: message.into(),
333            attribute: None,
334            value: None,
335        }
336    }
337
338    /// Create a new CapacityExceeded error.
339    pub fn capacity_exceeded(message: impl Into<String>) -> Self {
340        Self::CapacityExceeded {
341            message: message.into(),
342            current_count: None,
343            limit: None,
344        }
345    }
346
347    /// Create a new ConcurrentModification error.
348    pub fn concurrent_modification(
349        tenant_id: impl Into<String>,
350        resource_type: impl Into<String>,
351        id: impl Into<String>,
352    ) -> Self {
353        Self::ConcurrentModification {
354            tenant_id: tenant_id.into(),
355            resource_type: resource_type.into(),
356            id: id.into(),
357            expected_version: None,
358            actual_version: None,
359        }
360    }
361
362    /// Create a new Unavailable error.
363    pub fn unavailable(message: impl Into<String>) -> Self {
364        Self::Unavailable {
365            message: message.into(),
366            retry_after: None,
367        }
368    }
369
370    /// Create a new PermissionDenied error.
371    pub fn permission_denied(operation: impl Into<String>, resource: impl Into<String>) -> Self {
372        Self::PermissionDenied {
373            operation: operation.into(),
374            resource: resource.into(),
375        }
376    }
377
378    /// Create a new Timeout error.
379    pub fn timeout(operation: impl Into<String>, duration: std::time::Duration) -> Self {
380        Self::Timeout {
381            operation: operation.into(),
382            duration,
383        }
384    }
385
386    /// Create a new DataCorruption error.
387    pub fn data_corruption(
388        tenant_id: impl Into<String>,
389        resource_type: impl Into<String>,
390        details: impl Into<String>,
391    ) -> Self {
392        Self::DataCorruption {
393            tenant_id: tenant_id.into(),
394            resource_type: resource_type.into(),
395            id: None,
396            details: details.into(),
397        }
398    }
399
400    /// Create a new Configuration error.
401    pub fn configuration(message: impl Into<String>) -> Self {
402        Self::Configuration {
403            message: message.into(),
404            parameter: None,
405        }
406    }
407
408    /// Create a new Network error.
409    pub fn network(message: impl Into<String>) -> Self {
410        Self::Network {
411            message: message.into(),
412            endpoint: None,
413        }
414    }
415
416    /// Create a new Serialization error.
417    pub fn serialization(message: impl Into<String>) -> Self {
418        Self::Serialization {
419            message: message.into(),
420            data_type: None,
421        }
422    }
423
424    /// Create a new Internal error.
425    pub fn internal(message: impl Into<String>) -> Self {
426        Self::Internal {
427            message: message.into(),
428            source: None,
429        }
430    }
431
432    /// Create a new Internal error with a source error.
433    pub fn internal_with_source(
434        message: impl Into<String>,
435        source: Box<dyn std::error::Error + Send + Sync>,
436    ) -> Self {
437        Self::Internal {
438            message: message.into(),
439            source: Some(source),
440        }
441    }
442
443    /// Check if this error indicates a resource was not found.
444    pub fn is_not_found(&self) -> bool {
445        matches!(self, StorageError::ResourceNotFound { .. })
446    }
447
448    /// Check if this error indicates a conflict (resource already exists or concurrent modification).
449    pub fn is_conflict(&self) -> bool {
450        matches!(
451            self,
452            StorageError::ResourceAlreadyExists { .. }
453                | StorageError::ConcurrentModification { .. }
454        )
455    }
456
457    /// Check if this error indicates a temporary failure that might succeed on retry.
458    pub fn is_temporary(&self) -> bool {
459        matches!(
460            self,
461            StorageError::Unavailable { .. }
462                | StorageError::Timeout { .. }
463                | StorageError::Network { .. }
464        )
465    }
466
467    /// Check if this error indicates invalid input data.
468    pub fn is_invalid_input(&self) -> bool {
469        matches!(
470            self,
471            StorageError::InvalidData { .. } | StorageError::InvalidQuery { .. }
472        )
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn test_storage_error_display() {
482        let error = StorageError::resource_not_found("tenant1", "User", "123");
483        assert_eq!(error.to_string(), "Resource not found: tenant1/User/123");
484
485        let error = StorageError::invalid_data("malformed JSON");
486        assert_eq!(error.to_string(), "Invalid data: malformed JSON");
487
488        let error = StorageError::capacity_exceeded("disk full");
489        assert_eq!(error.to_string(), "Capacity exceeded: disk full");
490    }
491
492    #[test]
493    fn test_storage_error_type_checks() {
494        let not_found = StorageError::resource_not_found("tenant1", "User", "123");
495        assert!(not_found.is_not_found());
496        assert!(!not_found.is_conflict());
497        assert!(!not_found.is_temporary());
498
499        let conflict = StorageError::resource_already_exists("tenant1", "User", "123");
500        assert!(!conflict.is_not_found());
501        assert!(conflict.is_conflict());
502        assert!(!conflict.is_temporary());
503
504        let timeout = StorageError::timeout("query", std::time::Duration::from_secs(30));
505        assert!(!timeout.is_not_found());
506        assert!(!timeout.is_conflict());
507        assert!(timeout.is_temporary());
508
509        let invalid = StorageError::invalid_data("bad format");
510        assert!(invalid.is_invalid_input());
511    }
512
513    #[test]
514    fn test_storage_error_constructors() {
515        let error = StorageError::invalid_data_with_cause("parse error", "unexpected token");
516        if let StorageError::InvalidData { message, cause } = error {
517            assert_eq!(message, "parse error");
518            assert_eq!(cause, Some("unexpected token".to_string()));
519        } else {
520            panic!("Expected InvalidData error");
521        }
522
523        let error = StorageError::concurrent_modification("tenant1", "User", "123");
524        if let StorageError::ConcurrentModification {
525            tenant_id,
526            resource_type,
527            id,
528            ..
529        } = error
530        {
531            assert_eq!(tenant_id, "tenant1");
532            assert_eq!(resource_type, "User");
533            assert_eq!(id, "123");
534        } else {
535            panic!("Expected ConcurrentModification error");
536        }
537    }
538}