1use alloc::string::String;
31use alloc::vec::Vec;
32use thiserror::Error;
33
34#[cfg(feature = "std")]
35use std::io;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct ConfigError {
44 pub record_key: String,
48 pub url: Option<String>,
50 pub message: String,
52}
53
54impl ConfigError {
55 pub fn new(
57 record_key: impl Into<String>,
58 url: Option<String>,
59 message: impl Into<String>,
60 ) -> Self {
61 Self {
62 record_key: record_key.into(),
63 url,
64 message: message.into(),
65 }
66 }
67}
68
69impl core::fmt::Display for ConfigError {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 if !self.record_key.is_empty() {
73 write!(f, "record '{}'", self.record_key)?;
74 if let Some(url) = &self.url {
75 write!(f, " ({url})")?;
76 }
77 write!(f, ": ")?;
78 } else if let Some(url) = &self.url {
79 write!(f, "({url}): ")?;
80 }
81 write!(f, "{}", self.message)
82 }
83}
84
85fn join_config_errors(errors: &[ConfigError]) -> String {
87 use core::fmt::Write;
88 let mut out = String::new();
89 for (i, e) in errors.iter().enumerate() {
90 if i > 0 {
91 out.push_str("; ");
92 }
93 let _ = write!(out, "{e}");
94 }
95 out
96}
97
98#[derive(Debug, Error)]
106#[non_exhaustive]
107pub enum DbError {
108 #[error("Connection failed to {endpoint}: {reason}")]
111 ConnectionFailed { endpoint: String, reason: String },
112
113 #[error("Buffer full: {buffer_name} ({size} items)")]
116 BufferFull { size: u32, buffer_name: String },
117
118 #[error("Consumer lagged by {lag_count} messages")]
120 BufferLagged { lag_count: u64, buffer_name: String },
121
122 #[error("Buffer channel closed: {buffer_name}")]
124 BufferClosed { buffer_name: String },
125
126 #[error("Buffer empty: no pending values")]
128 BufferEmpty,
129
130 #[error("Record type not found: {record_name}")]
133 RecordNotFound { record_name: String },
134
135 #[error("Record key not found: {key}")]
137 RecordKeyNotFound { key: String },
138
139 #[error("Invalid record ID: {id}")]
141 InvalidRecordId { id: u32 },
142
143 #[error("Type mismatch: expected {expected_type}, record {record_id} has different type")]
145 TypeMismatch {
146 record_id: u32,
147 expected_type: String,
148 },
149
150 #[error("Invalid operation '{operation}': {reason}")]
152 InvalidOperation { operation: String, reason: String },
153
154 #[error("Permission denied: {operation}")]
156 PermissionDenied { operation: String },
157
158 #[error("Missing configuration parameter: {parameter}")]
161 MissingConfiguration { parameter: String },
162
163 #[error("invalid configuration ({} error(s)): {}", errors.len(), join_config_errors(errors))]
166 InvalidConfiguration { errors: Vec<ConfigError> },
167
168 #[error("Runtime error: {message}")]
171 RuntimeError { message: String },
172
173 #[error("Internal error (0x{code:04X}): {message}")]
176 Internal { code: u32, message: String },
177
178 #[error("Cyclic dependency detected among records: {records:?}")]
181 CyclicDependency { records: Vec<String> },
182
183 #[error("Transform on '{output_key}' references unregistered input '{input_key}'")]
185 TransformInputNotFound {
186 output_key: String,
187 input_key: String,
188 },
189
190 #[cfg(feature = "std")]
193 #[error("I/O error: {source}")]
194 Io {
195 #[from]
196 source: io::Error,
197 },
198
199 #[cfg(feature = "std")]
201 #[error("I/O error: {context}: {source}")]
202 IoWithContext {
203 context: String,
204 #[source]
205 source: io::Error,
206 },
207
208 #[cfg(feature = "std")]
210 #[error("JSON error: {source}")]
211 Json {
212 #[from]
213 source: serde_json::Error,
214 },
215
216 #[cfg(feature = "std")]
218 #[error("JSON error: {context}: {source}")]
219 JsonWithContext {
220 context: String,
221 #[source]
222 source: serde_json::Error,
223 },
224}
225
226impl DbError {
228 pub fn runtime_error(message: impl Into<String>) -> Self {
230 DbError::RuntimeError {
231 message: message.into(),
232 }
233 }
234
235 pub fn permission_denied(operation: impl Into<String>) -> Self {
237 DbError::PermissionDenied {
238 operation: operation.into(),
239 }
240 }
241
242 pub fn record_key_not_found(key: impl Into<String>) -> Self {
244 DbError::RecordKeyNotFound { key: key.into() }
245 }
246
247 pub fn missing_configuration(parameter: impl Into<String>) -> Self {
249 DbError::MissingConfiguration {
250 parameter: parameter.into(),
251 }
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
262#[non_exhaustive]
263pub enum DbErrorKind {
264 Retry,
266
267 Lagged,
269
270 Closed,
272
273 Transport,
275
276 Data,
278
279 Configuration,
281
282 Usage,
284
285 Internal,
287}
288
289impl DbError {
290 pub fn kind(&self) -> DbErrorKind {
295 match self {
296 Self::BufferEmpty | Self::BufferFull { .. } => DbErrorKind::Retry,
297
298 Self::BufferLagged { .. } => DbErrorKind::Lagged,
299
300 Self::BufferClosed { .. } => DbErrorKind::Closed,
301
302 Self::ConnectionFailed { .. } => DbErrorKind::Transport,
303
304 Self::MissingConfiguration { .. }
305 | Self::InvalidConfiguration { .. }
306 | Self::CyclicDependency { .. }
307 | Self::TransformInputNotFound { .. } => DbErrorKind::Configuration,
308
309 Self::RecordNotFound { .. }
310 | Self::RecordKeyNotFound { .. }
311 | Self::InvalidRecordId { .. }
312 | Self::TypeMismatch { .. }
313 | Self::InvalidOperation { .. }
314 | Self::PermissionDenied { .. } => DbErrorKind::Usage,
315
316 Self::RuntimeError { .. } | Self::Internal { .. } => DbErrorKind::Internal,
317
318 #[cfg(feature = "std")]
319 Self::Io { .. } | Self::IoWithContext { .. } => DbErrorKind::Transport,
320
321 #[cfg(feature = "std")]
322 Self::Json { .. } | Self::JsonWithContext { .. } => DbErrorKind::Data,
323 }
324 }
325}
326
327pub type DbResult<T> = Result<T, DbError>;
329
330impl From<crate::executor::ExecutorError> for DbError {
339 fn from(err: crate::executor::ExecutorError) -> Self {
340 use crate::executor::ExecutorError;
341
342 match err {
343 ExecutorError::QueueClosed => DbError::runtime_error("join queue closed"),
344 }
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use alloc::string::ToString;
352 use alloc::vec;
353
354 #[test]
355 fn test_error_size_constraint() {
356 let size = core::mem::size_of::<DbError>();
357 assert!(
358 size <= 64,
359 "DbError size ({} bytes) exceeds 64-byte embedded limit",
360 size
361 );
362 }
363
364 #[test]
365 fn test_invalid_configuration_display() {
366 let err = DbError::InvalidConfiguration {
367 errors: vec![
368 ConfigError::new(
369 "rec.a",
370 Some("mqtt://broker/a".to_string()),
371 "missing serializer",
372 ),
373 ConfigError::new("rec.b", None, "duplicate producer"),
374 ],
375 };
376
377 let s = err.to_string();
378 assert!(s.contains("2 error(s)"), "got: {s}");
379 assert!(
380 s.contains("record 'rec.a' (mqtt://broker/a): missing serializer"),
381 "got: {s}"
382 );
383 assert!(s.contains("record 'rec.b': duplicate producer"), "got: {s}");
384 }
385
386 #[test]
387 fn test_helper_constructors() {
388 assert!(matches!(
389 DbError::runtime_error("boom"),
390 DbError::RuntimeError { .. }
391 ));
392 assert!(matches!(
393 DbError::permission_denied("write"),
394 DbError::PermissionDenied { .. }
395 ));
396 assert!(matches!(
397 DbError::record_key_not_found("k"),
398 DbError::RecordKeyNotFound { .. }
399 ));
400 assert!(matches!(
401 DbError::missing_configuration("p"),
402 DbError::MissingConfiguration { .. }
403 ));
404 }
405
406 #[cfg(feature = "std")]
407 #[test]
408 fn test_io_json_conversions() {
409 let io_error = std::io::Error::other("File not found");
410 let db_error: DbError = io_error.into();
411 assert!(matches!(db_error, DbError::Io { .. }));
412
413 let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
414 let db_error: DbError = json_error.into();
415 assert!(matches!(db_error, DbError::Json { .. }));
416 }
417
418 #[test]
419 fn kind_maps_each_variant_to_its_action() {
420 use DbErrorKind::*;
421
422 assert_eq!(DbError::BufferEmpty.kind(), Retry);
423 assert_eq!(
424 DbError::BufferFull {
425 size: 8,
426 buffer_name: "b".to_string()
427 }
428 .kind(),
429 Retry
430 );
431
432 assert_eq!(
433 DbError::BufferLagged {
434 lag_count: 3,
435 buffer_name: "b".to_string()
436 }
437 .kind(),
438 Lagged
439 );
440
441 assert_eq!(
442 DbError::BufferClosed {
443 buffer_name: "b".to_string()
444 }
445 .kind(),
446 Closed
447 );
448
449 assert_eq!(
450 DbError::ConnectionFailed {
451 endpoint: "e".to_string(),
452 reason: "r".to_string()
453 }
454 .kind(),
455 Transport
456 );
457
458 assert_eq!(DbError::missing_configuration("p").kind(), Configuration);
459 assert_eq!(
460 DbError::InvalidConfiguration { errors: vec![] }.kind(),
461 Configuration
462 );
463 assert_eq!(
464 DbError::CyclicDependency { records: vec![] }.kind(),
465 Configuration
466 );
467 assert_eq!(
468 DbError::TransformInputNotFound {
469 output_key: "o".to_string(),
470 input_key: "i".to_string()
471 }
472 .kind(),
473 Configuration
474 );
475
476 assert_eq!(
477 DbError::RecordNotFound {
478 record_name: "r".to_string()
479 }
480 .kind(),
481 Usage
482 );
483 assert_eq!(DbError::record_key_not_found("k").kind(), Usage);
484 assert_eq!(DbError::InvalidRecordId { id: 9 }.kind(), Usage);
485 assert_eq!(
486 DbError::TypeMismatch {
487 expected_type: "T".to_string(),
488 record_id: 1
489 }
490 .kind(),
491 Usage
492 );
493 assert_eq!(
494 DbError::InvalidOperation {
495 operation: "o".to_string(),
496 reason: "r".to_string()
497 }
498 .kind(),
499 Usage
500 );
501 assert_eq!(DbError::permission_denied("op").kind(), Usage);
502
503 assert_eq!(DbError::runtime_error("m").kind(), Internal);
504 assert_eq!(
505 DbError::Internal {
506 code: 1,
507 message: "m".to_string()
508 }
509 .kind(),
510 Internal
511 );
512 }
513
514 #[cfg(feature = "std")]
515 #[test]
516 fn kind_maps_the_std_only_variants() {
517 let io: DbError = std::io::Error::other("x").into();
518 assert_eq!(io.kind(), DbErrorKind::Transport);
519
520 let json: DbError = serde_json::from_str::<serde_json::Value>("nope")
521 .unwrap_err()
522 .into();
523 assert_eq!(json.kind(), DbErrorKind::Data);
524 }
525
526 #[test]
528 fn retry_and_closed_stay_distinguishable() {
529 assert_ne!(
530 DbError::BufferEmpty.kind(),
531 DbError::BufferClosed {
532 buffer_name: "b".to_string()
533 }
534 .kind()
535 );
536 }
537}