1use nodedb_crdt::CrdtError;
10
11use super::LiteError;
12
13impl From<CrdtError> for LiteError {
16 fn from(e: CrdtError) -> Self {
17 match e {
18 CrdtError::ConstraintViolation {
19 constraint,
20 collection,
21 detail,
22 } => Self::ConstraintViolation {
23 collection,
24 constraint,
25 detail,
26 },
27 client @ (CrdtError::ImportTooLarge { .. }
31 | CrdtError::ImportMalformed { .. }
32 | CrdtError::ImportInvalidOperationRange
33 | CrdtError::ImportOperationLimitExceeded { .. }
34 | CrdtError::PreviewDeltaTooLarge { .. }
35 | CrdtError::PreviewMalformed { .. }
36 | CrdtError::PreviewInvalidOperationRange
37 | CrdtError::PreviewOperationLimitExceeded { .. }
38 | CrdtError::PreviewWriteSetLimitExceeded { .. }
39 | CrdtError::PreviewTargetMismatch { .. }
40 | CrdtError::PreviewPostImageTooLarge { .. }
41 | CrdtError::NonMapRootContainer { .. }
42 | CrdtError::NonMapRowValue { .. }
43 | CrdtError::BlockListPathUnresolved { .. }
44 | CrdtError::BlockListIndexOutOfBounds { .. }) => Self::DataException {
45 detail: client.to_string(),
46 },
47 CrdtError::ScalarFieldShadowsContainer { ref collection, .. } => Self::TypeMismatch {
50 collection: collection.clone(),
51 detail: e.to_string(),
52 },
53 client @ CrdtError::BlockListNotMovable { .. } => Self::TypeMismatch {
54 collection: String::new(),
55 detail: client.to_string(),
56 },
57 CrdtError::RowAbsentAtVersion { collection, row_id } => Self::UndefinedObject {
58 object: format!(
59 "document \"{row_id}\" in collection \"{collection}\" at the target version"
60 ),
61 },
62 CrdtError::BlockListRowAbsent { collection, row_id } => Self::UndefinedObject {
63 object: format!("document \"{row_id}\" in collection \"{collection}\""),
64 },
65 CrdtError::UnknownCollection(collection) => Self::UndefinedObject {
66 object: format!("collection \"{collection}\""),
67 },
68 client @ CrdtError::VersionBeforeCompactionBoundary { .. } => Self::ObjectNotReady {
69 object: "CRDT version".into(),
70 detail: client.to_string(),
71 },
72 retry @ (CrdtError::ImportPendingDependencies
75 | CrdtError::PreviewPendingDependencies
76 | CrdtError::DlqFull { .. }) => Self::RetryableRefusal {
77 detail: retry.to_string(),
78 },
79 denied @ (CrdtError::AuthExpired { .. } | CrdtError::InvalidSignature { .. }) => {
80 Self::AuthorizationDenied {
81 resource: denied.to_string(),
82 }
83 }
84 replay @ CrdtError::ReplayDetected { .. } => Self::PrevalidationRejected {
86 reason: replay.to_string(),
87 },
88 fault @ (CrdtError::DeltaApplyFailed(_)
91 | CrdtError::PreviewSourceTransactionPending { .. }
92 | CrdtError::Loro(_)) => Self::Storage {
93 detail: fault.to_string(),
94 },
95 }
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use nodedb_types::error::{ErrorCode, NodeDbError};
102
103 use super::*;
104
105 fn public(e: CrdtError) -> NodeDbError {
106 NodeDbError::from(LiteError::from(e))
107 }
108
109 #[test]
110 fn a_missing_document_or_collection_is_an_undefined_object() {
111 for e in [
112 CrdtError::RowAbsentAtVersion {
113 collection: "notes".into(),
114 row_id: "doc".into(),
115 },
116 CrdtError::BlockListRowAbsent {
117 collection: "pages".into(),
118 row_id: "p".into(),
119 },
120 CrdtError::UnknownCollection("c".into()),
121 ] {
122 assert_eq!(public(e).code(), ErrorCode::UNDEFINED_OBJECT);
123 }
124 }
125
126 #[test]
127 fn a_version_below_the_compaction_boundary_is_not_ready() {
128 let e = CrdtError::VersionBeforeCompactionBoundary {
129 peer: 1,
130 requested: 2,
131 discarded: 3,
132 };
133 assert_eq!(public(e).code(), ErrorCode::OBJECT_NOT_READY);
134 }
135
136 #[test]
137 fn malformed_client_input_is_a_data_exception() {
138 for e in [
139 CrdtError::ImportMalformed { detail: "x".into() },
140 CrdtError::ImportTooLarge {
141 limit: 1,
142 actual: 2,
143 },
144 CrdtError::PreviewPostImageTooLarge {
145 limit: 1,
146 actual: 2,
147 },
148 CrdtError::NonMapRowValue {
149 collection: "c".into(),
150 row_id: "r".into(),
151 value: "a List container".into(),
152 },
153 CrdtError::BlockListPathUnresolved {
154 list_path: "a.blocks".into(),
155 segment: "a".into(),
156 },
157 CrdtError::BlockListIndexOutOfBounds {
158 list_path: "blocks".into(),
159 index: 9,
160 len: 1,
161 },
162 ] {
163 assert_eq!(public(e).code(), ErrorCode::DATA_EXCEPTION);
164 }
165 }
166
167 #[test]
168 fn a_wrong_kind_of_value_is_a_type_mismatch() {
169 let shadow = LiteError::from(CrdtError::ScalarFieldShadowsContainer {
170 collection: "c".into(),
171 row_id: "r".into(),
172 field: "blocks".into(),
173 });
174 assert!(
175 matches!(&shadow, LiteError::TypeMismatch { collection, .. } if collection == "c"),
176 "{shadow:?}"
177 );
178 assert_eq!(NodeDbError::from(shadow).code(), ErrorCode::TYPE_MISMATCH);
179 let plain = CrdtError::BlockListNotMovable {
180 list_path: "blocks".into(),
181 };
182 assert_eq!(public(plain).code(), ErrorCode::TYPE_MISMATCH);
183 }
184
185 #[test]
186 fn missing_causal_history_and_a_full_queue_are_retriable() {
187 for e in [
188 CrdtError::ImportPendingDependencies,
189 CrdtError::PreviewPendingDependencies,
190 CrdtError::DlqFull {
191 capacity: 1,
192 pending: 1,
193 },
194 ] {
195 let mapped = public(e);
196 assert_eq!(mapped.code(), ErrorCode::WRITE_CONFLICT);
197 assert!(mapped.is_retriable(), "{mapped}");
198 }
199 }
200
201 #[test]
202 fn a_constraint_violation_keeps_its_constraint() {
203 let mapped = LiteError::from(CrdtError::ConstraintViolation {
204 constraint: "users_email_unique".into(),
205 collection: "users".into(),
206 detail: "duplicate".into(),
207 });
208 assert!(
209 matches!(&mapped, LiteError::ConstraintViolation { constraint, collection, .. }
210 if constraint == "users_email_unique" && collection == "users"),
211 "{mapped:?}"
212 );
213 assert!(NodeDbError::from(mapped).is_constraint_violation());
214 }
215
216 #[test]
217 fn auth_and_replay_refusals_keep_their_class() {
218 let expired = public(CrdtError::AuthExpired {
219 user_id: 1,
220 expired_at: 2,
221 });
222 assert!(expired.is_auth_denied(), "{expired}");
223 let replay = public(CrdtError::ReplayDetected {
224 user_id: 1,
225 device_id: 2,
226 seq_no: 3,
227 last_seen: 4,
228 });
229 assert_eq!(replay.code(), ErrorCode::PREVALIDATION_REJECTED);
230 }
231
232 #[test]
233 fn only_engine_faults_stay_storage() {
234 for e in [
235 CrdtError::PreviewSourceTransactionPending { operations: 1 },
236 CrdtError::Loro("boom".into()),
237 CrdtError::DeltaApplyFailed("boom".into()),
238 ] {
239 assert!(matches!(LiteError::from(e), LiteError::Storage { .. }));
240 }
241 }
242
243 #[test]
244 fn a_storage_class_crdt_error_names_its_operation() {
245 let e = LiteError::crdt("CRDT upsert", CrdtError::Loro("boom".into()));
246 assert!(
247 matches!(&e, LiteError::Storage { detail } if detail.starts_with("CRDT upsert: ")),
248 "{e:?}"
249 );
250 let client = LiteError::crdt("CRDT upsert", CrdtError::UnknownCollection("c".into()));
251 assert!(matches!(client, LiteError::UndefinedObject { .. }));
252 }
253}