1#[derive(Debug, Clone, thiserror::Error)]
14#[non_exhaustive]
15pub enum CoreError {
16 #[error(transparent)]
18 Validation(#[from] ValidationError),
19
20 #[error(transparent)]
22 Transition(#[from] TransitionError),
23
24 #[error(transparent)]
26 Sovereignty(#[from] SovereigntyError),
27}
28
29#[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 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 pub fn field(&self) -> &str {
51 &self.field
52 }
53
54 pub fn message(&self) -> &str {
56 &self.message
57 }
58}
59
60#[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 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 pub fn from_state(&self) -> &str {
82 &self.from
83 }
84
85 pub fn event(&self) -> &str {
87 &self.event
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
95#[error("sovereignty violation: {message}")]
96pub struct SovereigntyError {
97 message: String,
98}
99
100impl SovereigntyError {
101 pub fn new(message: impl Into<String>) -> Self {
103 Self {
104 message: message.into(),
105 }
106 }
107
108 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 assert_eq!(err.to_string(), cloned.to_string());
190 }
191}