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", "EntryNotFound") => BackendError::EntryNotFound {
48 id: extract_id_from_message(&err.message).unwrap_or_default(),
49 }
50 .into(),
51 ("backend", "VerificationStatusNotFound") => BackendError::VerificationStatusNotFound {
52 id: extract_id_from_message(&err.message).unwrap_or_default(),
53 }
54 .into(),
55 ("backend", "EntryNotInTree") => BackendError::EntryNotInTree {
56 entry_id: ID::default(),
57 tree_id: ID::default(),
58 }
59 .into(),
60 ("backend", "NoCommonAncestor") => {
61 BackendError::NoCommonAncestor { entry_ids: vec![] }.into()
62 }
63 ("backend", "EmptyEntryList") => BackendError::EmptyEntryList {
64 operation: err.message.clone(),
65 }
66 .into(),
67 ("instance", "DatabaseNotFound") => InstanceError::DatabaseNotFound {
68 name: err.message.clone(),
69 }
70 .into(),
71 ("instance", "EntryNotFound") => InstanceError::EntryNotFound {
72 entry_id: extract_id_from_message(&err.message).unwrap_or_default(),
73 }
74 .into(),
75 ("instance", "InstanceAlreadyExists") => InstanceError::InstanceAlreadyExists.into(),
76 ("instance", "DeviceKeyNotFound") => InstanceError::DeviceKeyNotFound.into(),
77 ("instance", "AuthenticationRequired") => InstanceError::AuthenticationRequired.into(),
78 _ => {
79 crate::Error::Io(std::io::Error::other(format!(
81 "[{}::{}] {}",
82 err.module, err.kind, err.message
83 )))
84 }
85 }
86}
87
88fn extract_id_from_message(message: &str) -> Option<ID> {
90 message
91 .rsplit(": ")
92 .next()
93 .and_then(|s| ID::parse(s.trim()).ok())
94}
95
96fn error_kind_name(err: &crate::Error) -> String {
98 match err {
99 crate::Error::Io(_) => "Io".to_string(),
100 crate::Error::Serialize(_) => "Serialize".to_string(),
101 crate::Error::Auth(e) => format!("{e:?}")
102 .split_once(|c: char| !c.is_alphanumeric())
103 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
104 crate::Error::Backend(e) => format!("{e:?}")
105 .split_once(|c: char| !c.is_alphanumeric())
106 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
107 crate::Error::Instance(e) => format!("{e:?}")
108 .split_once(|c: char| !c.is_alphanumeric())
109 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
110 crate::Error::CRDT(e) => format!("{e:?}")
111 .split_once(|c: char| !c.is_alphanumeric())
112 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
113 crate::Error::Store(e) => format!("{e:?}")
114 .split_once(|c: char| !c.is_alphanumeric())
115 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
116 crate::Error::Transaction(e) => format!("{e:?}")
117 .split_once(|c: char| !c.is_alphanumeric())
118 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
119 crate::Error::Sync(e) => format!("{e:?}")
120 .split_once(|c: char| !c.is_alphanumeric())
121 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
122 crate::Error::Entry(e) => format!("{e:?}")
123 .split_once(|c: char| !c.is_alphanumeric())
124 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
125 crate::Error::Id(e) => format!("{e:?}")
126 .split_once(|c: char| !c.is_alphanumeric())
127 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
128 crate::Error::User(e) => format!("{e:?}")
129 .split_once(|c: char| !c.is_alphanumeric())
130 .map_or_else(|| format!("{e:?}"), |(name, _)| name.to_string()),
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn test_service_error_from_backend_not_found() {
140 let test_id = ID::from_bytes("abc123");
141 let err = crate::Error::Backend(Box::new(BackendError::EntryNotFound { id: test_id }));
142 let se = ServiceError::from(&err);
143 assert_eq!(se.module, "backend");
144 assert_eq!(se.kind, "EntryNotFound");
145 assert!(se.message.contains("abc123") || se.message.contains("Entry not found"));
146 }
147
148 #[test]
149 fn test_service_error_from_instance_error() {
150 let err = crate::Error::Instance(Box::new(InstanceError::DeviceKeyNotFound));
151 let se = ServiceError::from(&err);
152 assert_eq!(se.module, "instance");
153 assert_eq!(se.kind, "DeviceKeyNotFound");
154 }
155
156 #[test]
157 fn test_roundtrip_backend_entry_not_found() {
158 let original = crate::Error::Backend(Box::new(BackendError::EntryNotFound {
159 id: ID::from_bytes("test-id"),
160 }));
161 let se = ServiceError::from(&original);
162 let reconstructed = service_error_to_eidetica_error(se);
163 assert!(reconstructed.is_not_found());
164 }
165
166 #[test]
167 fn test_roundtrip_instance_already_exists() {
168 let original = crate::Error::Instance(Box::new(InstanceError::InstanceAlreadyExists));
169 let se = ServiceError::from(&original);
170 let reconstructed = service_error_to_eidetica_error(se);
171 assert!(reconstructed.is_conflict());
172 }
173
174 #[test]
175 fn test_unknown_error_falls_back_to_io() {
176 let se = ServiceError {
177 module: "unknown".to_string(),
178 kind: "SomethingWeird".to_string(),
179 message: "something happened".to_string(),
180 };
181 let err = service_error_to_eidetica_error(se);
182 assert!(err.is_io_error());
183 }
184
185 #[test]
197 fn test_all_mapped_pairs_roundtrip_module_and_kind() {
198 let cases: Vec<crate::Error> = vec![
199 crate::Error::Backend(Box::new(BackendError::EntryNotFound {
200 id: ID::from_bytes("rt-entry"),
201 })),
202 crate::Error::Backend(Box::new(BackendError::VerificationStatusNotFound {
203 id: ID::from_bytes("rt-vs"),
204 })),
205 crate::Error::Backend(Box::new(BackendError::EntryNotInTree {
206 entry_id: ID::from_bytes("rt-e"),
207 tree_id: ID::from_bytes("rt-t"),
208 })),
209 crate::Error::Backend(Box::new(BackendError::NoCommonAncestor {
210 entry_ids: vec![ID::from_bytes("rt-a")],
211 })),
212 crate::Error::Backend(Box::new(BackendError::EmptyEntryList {
213 operation: "rt".to_string(),
214 })),
215 crate::Error::Instance(Box::new(InstanceError::DatabaseNotFound {
216 name: "rt-db".to_string(),
217 })),
218 crate::Error::Instance(Box::new(InstanceError::EntryNotFound {
219 entry_id: ID::from_bytes("rt-ie"),
220 })),
221 crate::Error::Instance(Box::new(InstanceError::InstanceAlreadyExists)),
222 crate::Error::Instance(Box::new(InstanceError::DeviceKeyNotFound)),
223 crate::Error::Instance(Box::new(InstanceError::AuthenticationRequired)),
224 ];
225
226 for original in &cases {
227 let se = ServiceError::from(original);
228 let reconstructed = service_error_to_eidetica_error(se.clone());
229 let round = ServiceError::from(&reconstructed);
230 assert_eq!(
231 (round.module.as_str(), round.kind.as_str()),
232 (se.module.as_str(), se.kind.as_str()),
233 "mapped pair ({}::{}) fell through the wildcard on reconstruction \
234 — its mapping arm or discriminant name has drifted",
235 se.module,
236 se.kind,
237 );
238 }
239 }
240
241 #[allow(dead_code)]
247 fn wire_mapping_exhaustiveness_guard(err: &crate::Error) {
248 match err {
249 crate::Error::Io(_) => {}
250 crate::Error::Serialize(_) => {}
251 crate::Error::Auth(_) => {}
252 crate::Error::Backend(_) => {}
253 crate::Error::Instance(_) => {}
254 crate::Error::CRDT(_) => {}
255 crate::Error::Store(_) => {}
256 crate::Error::Transaction(_) => {}
257 crate::Error::Sync(_) => {}
258 crate::Error::Entry(_) => {}
259 crate::Error::Id(_) => {}
260 crate::Error::User(_) => {}
261 }
262 }
263
264 #[test]
265 fn test_service_error_serde_roundtrip() {
266 let se = ServiceError {
267 module: "backend".to_string(),
268 kind: "EntryNotFound".to_string(),
269 message: "Entry not found: test123".to_string(),
270 };
271 let json = serde_json::to_string(&se).unwrap();
272 let deserialized: ServiceError = serde_json::from_str(&json).unwrap();
273 assert_eq!(deserialized.module, se.module);
274 assert_eq!(deserialized.kind, se.kind);
275 assert_eq!(deserialized.message, se.message);
276 }
277}