1use serde::{Deserialize, Serialize};
8
9use crate::backend::BackendError;
10use crate::entry::ID;
11use crate::instance::InstanceError;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ServiceError {
19 pub module: String,
21 pub kind: String,
23 pub message: String,
25}
26
27impl From<&crate::Error> for ServiceError {
28 fn from(err: &crate::Error) -> Self {
29 let module = err.module().to_string();
30 let kind = error_kind_name(err);
31 let message = err.to_string();
32 ServiceError {
33 module,
34 kind,
35 message,
36 }
37 }
38}
39
40pub fn service_error_to_eidetica_error(err: ServiceError) -> crate::Error {
46 match (err.module.as_str(), err.kind.as_str()) {
47 ("backend", "InvalidStoreStateStagingToken") => {
48 BackendError::InvalidStoreStateStagingToken.into()
49 }
50 ("backend", "InvalidStoreStateView") => BackendError::InvalidStoreStateView.into(),
51 ("backend", "RecordTooLarge") => BackendError::RecordTooLarge { encoded_bytes: 0 }.into(),
52 ("backend", "EntryNotFound") => BackendError::EntryNotFound {
53 id: extract_id_from_message(&err.message).unwrap_or_default(),
54 }
55 .into(),
56 ("backend", "VerificationStatusNotFound") => BackendError::VerificationStatusNotFound {
57 id: extract_id_from_message(&err.message).unwrap_or_default(),
58 }
59 .into(),
60 ("backend", "EntryNotInTree") => BackendError::EntryNotInTree {
61 entry_id: ID::default(),
62 tree_id: ID::default(),
63 }
64 .into(),
65 ("backend", "NoCommonAncestor") => {
68 BackendError::NoCommonAncestor { entry_ids: vec![] }.into()
69 }
70 ("backend", "EmptyEntryList") => BackendError::EmptyEntryList {
71 operation: err.message.clone(),
72 }
73 .into(),
74 ("instance", "DatabaseNotFound") => InstanceError::DatabaseNotFound {
75 name: err.message.clone(),
76 }
77 .into(),
78 ("instance", "EntryNotFound") => InstanceError::EntryNotFound {
79 entry_id: extract_id_from_message(&err.message).unwrap_or_default(),
80 }
81 .into(),
82 ("instance", "InstanceAlreadyExists") => InstanceError::InstanceAlreadyExists.into(),
83 ("instance", "DeviceKeyNotFound") => InstanceError::DeviceKeyNotFound.into(),
84 ("instance", "AuthenticationRequired") => InstanceError::AuthenticationRequired.into(),
85 _ => {
86 crate::Error::Io(std::io::Error::other(format!(
88 "[{}::{}] {}",
89 err.module, err.kind, err.message
90 )))
91 }
92 }
93}
94
95fn extract_id_from_message(message: &str) -> Option<ID> {
97 message
98 .rsplit(": ")
99 .next()
100 .and_then(|s| ID::parse(s.trim()).ok())
101}
102
103fn error_kind_name(err: &crate::Error) -> String {
105 match err {
106 crate::Error::Io(_) => "Io".to_string(),
107 crate::Error::Serialize(_) => "Serialize".to_string(),
108 crate::Error::Auth(e) => format!("{e:?}")
109 .split_once(|c: char| !c.is_alphanumeric())
110 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
111 crate::Error::Backend(e) => format!("{e:?}")
112 .split_once(|c: char| !c.is_alphanumeric())
113 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
114 crate::Error::Instance(e) => format!("{e:?}")
115 .split_once(|c: char| !c.is_alphanumeric())
116 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
117 crate::Error::CRDT(e) => format!("{e:?}")
118 .split_once(|c: char| !c.is_alphanumeric())
119 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
120 crate::Error::Store(e) => format!("{e:?}")
121 .split_once(|c: char| !c.is_alphanumeric())
122 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
123 crate::Error::Transaction(e) => format!("{e:?}")
124 .split_once(|c: char| !c.is_alphanumeric())
125 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
126 crate::Error::Sync(e) => format!("{e:?}")
127 .split_once(|c: char| !c.is_alphanumeric())
128 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
129 crate::Error::Entry(e) => format!("{e:?}")
130 .split_once(|c: char| !c.is_alphanumeric())
131 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
132 crate::Error::Id(e) => format!("{e:?}")
133 .split_once(|c: char| !c.is_alphanumeric())
134 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
135 crate::Error::User(e) => format!("{e:?}")
136 .split_once(|c: char| !c.is_alphanumeric())
137 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn test_service_error_from_backend_not_found() {
147 let test_id = ID::from_bytes("abc123");
148 let err = crate::Error::Backend(Box::new(BackendError::EntryNotFound { id: test_id }));
149 let se = ServiceError::from(&err);
150 assert_eq!(se.module, "backend");
151 assert_eq!(se.kind, "EntryNotFound");
152 assert!(se.message.contains("abc123") || se.message.contains("Entry not found"));
153 }
154
155 #[test]
156 fn test_service_error_from_instance_error() {
157 let err = crate::Error::Instance(Box::new(InstanceError::DeviceKeyNotFound));
158 let se = ServiceError::from(&err);
159 assert_eq!(se.module, "instance");
160 assert_eq!(se.kind, "DeviceKeyNotFound");
161 }
162
163 #[test]
164 fn test_roundtrip_backend_entry_not_found() {
165 let original = crate::Error::Backend(Box::new(BackendError::EntryNotFound {
166 id: ID::from_bytes("test-id"),
167 }));
168 let se = ServiceError::from(&original);
169 let reconstructed = service_error_to_eidetica_error(se);
170 assert!(reconstructed.is_not_found());
171 }
172
173 #[test]
174 fn test_roundtrip_instance_already_exists() {
175 let original = crate::Error::Instance(Box::new(InstanceError::InstanceAlreadyExists));
176 let se = ServiceError::from(&original);
177 let reconstructed = service_error_to_eidetica_error(se);
178 assert!(reconstructed.is_conflict());
179 }
180
181 #[test]
182 fn test_unknown_error_falls_back_to_io() {
183 let se = ServiceError {
184 module: "unknown".to_string(),
185 kind: "SomethingWeird".to_string(),
186 message: "something happened".to_string(),
187 };
188 let err = service_error_to_eidetica_error(se);
189 assert!(err.is_io_error());
190 }
191
192 #[test]
193 fn test_store_state_view_and_staging_token_stay_distinct_on_wire() {
194 let view = crate::Error::Backend(Box::new(BackendError::InvalidStoreStateView));
199 let view_round = service_error_to_eidetica_error(ServiceError::from(&view));
200 assert!(view_round.is_invalid_store_state_view());
201
202 let staging = crate::Error::Backend(Box::new(BackendError::InvalidStoreStateStagingToken));
203 let staging_round = service_error_to_eidetica_error(ServiceError::from(&staging));
204 assert!(
205 matches!(
206 staging_round,
207 crate::Error::Backend(ref e)
208 if matches!(**e, BackendError::InvalidStoreStateStagingToken)
209 ),
210 "staging-token failure must stay a staging-token error, got {staging_round:?}"
211 );
212 assert!(!staging_round.is_invalid_store_state_view());
213 }
214
215 #[test]
227 fn test_all_mapped_pairs_roundtrip_module_and_kind() {
228 let cases: Vec<crate::Error> = vec![
229 crate::Error::Backend(Box::new(BackendError::InvalidStoreStateStagingToken)),
230 crate::Error::Backend(Box::new(BackendError::InvalidStoreStateView)),
231 crate::Error::Backend(Box::new(BackendError::RecordTooLarge { encoded_bytes: 42 })),
232 crate::Error::Backend(Box::new(BackendError::EntryNotFound {
233 id: ID::from_bytes("rt-entry"),
234 })),
235 crate::Error::Backend(Box::new(BackendError::VerificationStatusNotFound {
236 id: ID::from_bytes("rt-vs"),
237 })),
238 crate::Error::Backend(Box::new(BackendError::EntryNotInTree {
239 entry_id: ID::from_bytes("rt-e"),
240 tree_id: ID::from_bytes("rt-t"),
241 })),
242 crate::Error::Backend(Box::new(BackendError::NoCommonAncestor {
243 entry_ids: vec![ID::from_bytes("rt-a")],
244 })),
245 crate::Error::Backend(Box::new(BackendError::EmptyEntryList {
246 operation: "rt".to_string(),
247 })),
248 crate::Error::Instance(Box::new(InstanceError::DatabaseNotFound {
249 name: "rt-db".to_string(),
250 })),
251 crate::Error::Instance(Box::new(InstanceError::EntryNotFound {
252 entry_id: ID::from_bytes("rt-ie"),
253 })),
254 crate::Error::Instance(Box::new(InstanceError::InstanceAlreadyExists)),
255 crate::Error::Instance(Box::new(InstanceError::DeviceKeyNotFound)),
256 crate::Error::Instance(Box::new(InstanceError::AuthenticationRequired)),
257 ];
258
259 for original in &cases {
260 let se = ServiceError::from(original);
261 let reconstructed = service_error_to_eidetica_error(se.clone());
262 let round = ServiceError::from(&reconstructed);
263 assert_eq!(
264 (round.module.as_str(), round.kind.as_str()),
265 (se.module.as_str(), se.kind.as_str()),
266 "mapped pair ({}::{}) fell through the wildcard on reconstruction \
267 — its mapping arm or discriminant name has drifted",
268 se.module,
269 se.kind,
270 );
271 }
272 }
273
274 #[allow(dead_code)]
280 fn wire_mapping_exhaustiveness_guard(err: &crate::Error) {
281 match err {
282 crate::Error::Io(_) => {}
283 crate::Error::Serialize(_) => {}
284 crate::Error::Auth(_) => {}
285 crate::Error::Backend(_) => {}
286 crate::Error::Instance(_) => {}
287 crate::Error::CRDT(_) => {}
288 crate::Error::Store(_) => {}
289 crate::Error::Transaction(_) => {}
290 crate::Error::Sync(_) => {}
291 crate::Error::Entry(_) => {}
292 crate::Error::Id(_) => {}
293 crate::Error::User(_) => {}
294 }
295 }
296
297 #[test]
298 fn test_service_error_serde_roundtrip() {
299 let se = ServiceError {
300 module: "backend".to_string(),
301 kind: "EntryNotFound".to_string(),
302 message: "Entry not found: test123".to_string(),
303 };
304 let json = serde_json::to_string(&se).unwrap();
305 let deserialized: ServiceError = serde_json::from_str(&json).unwrap();
306 assert_eq!(deserialized.module, se.module);
307 assert_eq!(deserialized.kind, se.kind);
308 assert_eq!(deserialized.message, se.message);
309 }
310}