Skip to main content

alpe_core/
error.rs

1//! Unified error hierarchy for the Alpe platform.
2//!
3//! Provides a layered error structure:
4//! - `CoreError` — top-level enum bridging all domain errors
5//! - `ValidationError` — input validation failures with field context
6//! - `TransitionError` — invalid state machine transitions
7//! - `SovereigntyError` — jurisdiction replication violations
8
9/// Top-level error type for the `alpe-core` crate.
10///
11/// Aggregates all domain-specific error variants and provides `From`
12/// conversions for ergonomic error propagation with `?`.
13#[derive(Debug, Clone, thiserror::Error)]
14#[non_exhaustive]
15pub enum CoreError {
16    /// An input validation rule was violated.
17    #[error(transparent)]
18    Validation(#[from] ValidationError),
19
20    /// An invalid state machine transition was attempted.
21    #[error(transparent)]
22    Transition(#[from] TransitionError),
23
24    /// A sovereignty / jurisdiction constraint was violated.
25    #[error(transparent)]
26    Sovereignty(#[from] SovereigntyError),
27}
28
29/// Error returned when an input value fails validation.
30///
31/// Carries the field name and a human-readable message describing the violation.
32/// Fields are private to enforce construction through [`ValidationError::new`].
33#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
34#[error("validation error on '{field}': {message}")]
35pub struct ValidationError {
36    field: String,
37    message: String,
38}
39
40impl ValidationError {
41    /// Creates a new validation error for the given field.
42    pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
43        Self {
44            field: field.into(),
45            message: message.into(),
46        }
47    }
48
49    /// Returns the name of the field that failed validation.
50    pub fn field(&self) -> &str {
51        &self.field
52    }
53
54    /// Returns the human-readable description of the validation failure.
55    pub fn message(&self) -> &str {
56        &self.message
57    }
58}
59
60/// Error returned when an invalid state machine transition is attempted.
61///
62/// Contains the source state and the event that was rejected.
63/// Fields are private to enforce construction through [`TransitionError::new`].
64#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
65#[error("invalid transition: cannot apply '{event}' in state '{from}'")]
66pub struct TransitionError {
67    from: String,
68    event: String,
69}
70
71impl TransitionError {
72    /// Creates a new transition error for the given state and event.
73    pub fn new(from: impl Into<String>, event: impl Into<String>) -> Self {
74        Self {
75            from: from.into(),
76            event: event.into(),
77        }
78    }
79
80    /// Returns the state the resource was in when the invalid transition was attempted.
81    pub fn from_state(&self) -> &str {
82        &self.from
83    }
84
85    /// Returns the event that was rejected.
86    pub fn event(&self) -> &str {
87        &self.event
88    }
89}
90
91/// Error returned when a sovereignty or jurisdiction constraint is violated.
92///
93/// Fields are private to enforce construction through [`SovereigntyError::new`].
94#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95#[error("sovereignty violation: {message}")]
96pub struct SovereigntyError {
97    message: String,
98}
99
100impl SovereigntyError {
101    /// Creates a new sovereignty error with the given description.
102    pub fn new(message: impl Into<String>) -> Self {
103        Self {
104            message: message.into(),
105        }
106    }
107
108    /// Returns the human-readable description of the sovereignty violation.
109    pub fn message(&self) -> &str {
110        &self.message
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn validation_error_display_is_actionable() {
120        let err = ValidationError::new("project_name", "must not be empty");
121        let msg = err.to_string();
122        assert!(msg.contains("project_name"), "should mention the field");
123        assert!(
124            msg.contains("must not be empty"),
125            "should mention the reason"
126        );
127    }
128
129    #[test]
130    fn validation_error_accessors() {
131        let err = ValidationError::new("email", "invalid format");
132        assert_eq!(err.field(), "email");
133        assert_eq!(err.message(), "invalid format");
134    }
135
136    #[test]
137    fn transition_error_display_contains_state_and_event() {
138        let err = TransitionError::new("Running", "ProvisionStarted");
139        let msg = err.to_string();
140        assert!(msg.contains("Running"));
141        assert!(msg.contains("ProvisionStarted"));
142    }
143
144    #[test]
145    fn transition_error_accessors() {
146        let err = TransitionError::new("Pending", "Deploy");
147        assert_eq!(err.from_state(), "Pending");
148        assert_eq!(err.event(), "Deploy");
149    }
150
151    #[test]
152    fn sovereignty_error_display_is_descriptive() {
153        let err = SovereigntyError::new("DE cannot replicate to FR");
154        assert!(err.to_string().contains("DE cannot replicate to FR"));
155    }
156
157    #[test]
158    fn sovereignty_error_accessor() {
159        let err = SovereigntyError::new("cross-country replication");
160        assert_eq!(err.message(), "cross-country replication");
161    }
162
163    #[test]
164    fn core_error_from_validation() {
165        let val_err = ValidationError::new("name", "too short");
166        let core_err: CoreError = val_err.into();
167        assert!(matches!(core_err, CoreError::Validation(_)));
168    }
169
170    #[test]
171    fn core_error_from_transition() {
172        let trans_err = TransitionError::new("Pending", "UpdateCompleted");
173        let core_err: CoreError = trans_err.into();
174        assert!(matches!(core_err, CoreError::Transition(_)));
175    }
176
177    #[test]
178    fn core_error_from_sovereignty() {
179        let sov_err = SovereigntyError::new("cross-country replication");
180        let core_err: CoreError = sov_err.into();
181        assert!(matches!(core_err, CoreError::Sovereignty(_)));
182    }
183
184    #[test]
185    fn core_error_is_clone() {
186        let err = CoreError::from(ValidationError::new("x", "y"));
187        let cloned = err.clone();
188        // Verify the clone produces the same Display output
189        assert_eq!(err.to_string(), cloned.to_string());
190    }
191}