use alloc::string::String;
use alloc::vec::Vec;
use thiserror::Error;
#[cfg(feature = "std")]
use std::io;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigError {
pub record_key: String,
pub url: Option<String>,
pub message: String,
}
impl ConfigError {
pub fn new(
record_key: impl Into<String>,
url: Option<String>,
message: impl Into<String>,
) -> Self {
Self {
record_key: record_key.into(),
url,
message: message.into(),
}
}
}
impl core::fmt::Display for ConfigError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if !self.record_key.is_empty() {
write!(f, "record '{}'", self.record_key)?;
if let Some(url) = &self.url {
write!(f, " ({url})")?;
}
write!(f, ": ")?;
} else if let Some(url) = &self.url {
write!(f, "({url}): ")?;
}
write!(f, "{}", self.message)
}
}
fn join_config_errors(errors: &[ConfigError]) -> String {
use core::fmt::Write;
let mut out = String::new();
for (i, e) in errors.iter().enumerate() {
if i > 0 {
out.push_str("; ");
}
let _ = write!(out, "{e}");
}
out
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum DbError {
#[error("Connection failed to {endpoint}: {reason}")]
ConnectionFailed { endpoint: String, reason: String },
#[error("Buffer full: {buffer_name} ({size} items)")]
BufferFull { size: u32, buffer_name: String },
#[error("Consumer lagged by {lag_count} messages")]
BufferLagged { lag_count: u64, buffer_name: String },
#[error("Buffer channel closed: {buffer_name}")]
BufferClosed { buffer_name: String },
#[error("Buffer empty: no pending values")]
BufferEmpty,
#[error("Record type not found: {record_name}")]
RecordNotFound { record_name: String },
#[error("Record key not found: {key}")]
RecordKeyNotFound { key: String },
#[error("Invalid record ID: {id}")]
InvalidRecordId { id: u32 },
#[error("Type mismatch: expected {expected_type}, record {record_id} has different type")]
TypeMismatch {
record_id: u32,
expected_type: String,
},
#[error("Invalid operation '{operation}': {reason}")]
InvalidOperation { operation: String, reason: String },
#[error("Permission denied: {operation}")]
PermissionDenied { operation: String },
#[error("Missing configuration parameter: {parameter}")]
MissingConfiguration { parameter: String },
#[error("invalid configuration ({} error(s)): {}", errors.len(), join_config_errors(errors))]
InvalidConfiguration { errors: Vec<ConfigError> },
#[error("Runtime error: {message}")]
RuntimeError { message: String },
#[error("Internal error (0x{code:04X}): {message}")]
Internal { code: u32, message: String },
#[error("Cyclic dependency detected among records: {records:?}")]
CyclicDependency { records: Vec<String> },
#[error("Transform on '{output_key}' references unregistered input '{input_key}'")]
TransformInputNotFound {
output_key: String,
input_key: String,
},
#[cfg(feature = "std")]
#[error("I/O error: {source}")]
Io {
#[from]
source: io::Error,
},
#[cfg(feature = "std")]
#[error("I/O error: {context}: {source}")]
IoWithContext {
context: String,
#[source]
source: io::Error,
},
#[cfg(feature = "std")]
#[error("JSON error: {source}")]
Json {
#[from]
source: serde_json::Error,
},
#[cfg(feature = "std")]
#[error("JSON error: {context}: {source}")]
JsonWithContext {
context: String,
#[source]
source: serde_json::Error,
},
}
impl DbError {
pub fn runtime_error(message: impl Into<String>) -> Self {
DbError::RuntimeError {
message: message.into(),
}
}
pub fn permission_denied(operation: impl Into<String>) -> Self {
DbError::PermissionDenied {
operation: operation.into(),
}
}
pub fn record_key_not_found(key: impl Into<String>) -> Self {
DbError::RecordKeyNotFound { key: key.into() }
}
pub fn missing_configuration(parameter: impl Into<String>) -> Self {
DbError::MissingConfiguration {
parameter: parameter.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DbErrorKind {
Retry,
Lagged,
Closed,
Transport,
Data,
Configuration,
Usage,
Internal,
}
impl DbError {
pub fn kind(&self) -> DbErrorKind {
match self {
Self::BufferEmpty | Self::BufferFull { .. } => DbErrorKind::Retry,
Self::BufferLagged { .. } => DbErrorKind::Lagged,
Self::BufferClosed { .. } => DbErrorKind::Closed,
Self::ConnectionFailed { .. } => DbErrorKind::Transport,
Self::MissingConfiguration { .. }
| Self::InvalidConfiguration { .. }
| Self::CyclicDependency { .. }
| Self::TransformInputNotFound { .. } => DbErrorKind::Configuration,
Self::RecordNotFound { .. }
| Self::RecordKeyNotFound { .. }
| Self::InvalidRecordId { .. }
| Self::TypeMismatch { .. }
| Self::InvalidOperation { .. }
| Self::PermissionDenied { .. } => DbErrorKind::Usage,
Self::RuntimeError { .. } | Self::Internal { .. } => DbErrorKind::Internal,
#[cfg(feature = "std")]
Self::Io { .. } | Self::IoWithContext { .. } => DbErrorKind::Transport,
#[cfg(feature = "std")]
Self::Json { .. } | Self::JsonWithContext { .. } => DbErrorKind::Data,
}
}
}
pub type DbResult<T> = Result<T, DbError>;
impl From<crate::executor::ExecutorError> for DbError {
fn from(err: crate::executor::ExecutorError) -> Self {
use crate::executor::ExecutorError;
match err {
ExecutorError::QueueClosed => DbError::runtime_error("join queue closed"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec;
#[test]
fn test_error_size_constraint() {
let size = core::mem::size_of::<DbError>();
assert!(
size <= 64,
"DbError size ({} bytes) exceeds 64-byte embedded limit",
size
);
}
#[test]
fn test_invalid_configuration_display() {
let err = DbError::InvalidConfiguration {
errors: vec![
ConfigError::new(
"rec.a",
Some("mqtt://broker/a".to_string()),
"missing serializer",
),
ConfigError::new("rec.b", None, "duplicate producer"),
],
};
let s = err.to_string();
assert!(s.contains("2 error(s)"), "got: {s}");
assert!(
s.contains("record 'rec.a' (mqtt://broker/a): missing serializer"),
"got: {s}"
);
assert!(s.contains("record 'rec.b': duplicate producer"), "got: {s}");
}
#[test]
fn test_helper_constructors() {
assert!(matches!(
DbError::runtime_error("boom"),
DbError::RuntimeError { .. }
));
assert!(matches!(
DbError::permission_denied("write"),
DbError::PermissionDenied { .. }
));
assert!(matches!(
DbError::record_key_not_found("k"),
DbError::RecordKeyNotFound { .. }
));
assert!(matches!(
DbError::missing_configuration("p"),
DbError::MissingConfiguration { .. }
));
}
#[cfg(feature = "std")]
#[test]
fn test_io_json_conversions() {
let io_error = std::io::Error::other("File not found");
let db_error: DbError = io_error.into();
assert!(matches!(db_error, DbError::Io { .. }));
let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let db_error: DbError = json_error.into();
assert!(matches!(db_error, DbError::Json { .. }));
}
#[test]
fn kind_maps_each_variant_to_its_action() {
use DbErrorKind::*;
assert_eq!(DbError::BufferEmpty.kind(), Retry);
assert_eq!(
DbError::BufferFull {
size: 8,
buffer_name: "b".to_string()
}
.kind(),
Retry
);
assert_eq!(
DbError::BufferLagged {
lag_count: 3,
buffer_name: "b".to_string()
}
.kind(),
Lagged
);
assert_eq!(
DbError::BufferClosed {
buffer_name: "b".to_string()
}
.kind(),
Closed
);
assert_eq!(
DbError::ConnectionFailed {
endpoint: "e".to_string(),
reason: "r".to_string()
}
.kind(),
Transport
);
assert_eq!(DbError::missing_configuration("p").kind(), Configuration);
assert_eq!(
DbError::InvalidConfiguration { errors: vec![] }.kind(),
Configuration
);
assert_eq!(
DbError::CyclicDependency { records: vec![] }.kind(),
Configuration
);
assert_eq!(
DbError::TransformInputNotFound {
output_key: "o".to_string(),
input_key: "i".to_string()
}
.kind(),
Configuration
);
assert_eq!(
DbError::RecordNotFound {
record_name: "r".to_string()
}
.kind(),
Usage
);
assert_eq!(DbError::record_key_not_found("k").kind(), Usage);
assert_eq!(DbError::InvalidRecordId { id: 9 }.kind(), Usage);
assert_eq!(
DbError::TypeMismatch {
expected_type: "T".to_string(),
record_id: 1
}
.kind(),
Usage
);
assert_eq!(
DbError::InvalidOperation {
operation: "o".to_string(),
reason: "r".to_string()
}
.kind(),
Usage
);
assert_eq!(DbError::permission_denied("op").kind(), Usage);
assert_eq!(DbError::runtime_error("m").kind(), Internal);
assert_eq!(
DbError::Internal {
code: 1,
message: "m".to_string()
}
.kind(),
Internal
);
}
#[cfg(feature = "std")]
#[test]
fn kind_maps_the_std_only_variants() {
let io: DbError = std::io::Error::other("x").into();
assert_eq!(io.kind(), DbErrorKind::Transport);
let json: DbError = serde_json::from_str::<serde_json::Value>("nope")
.unwrap_err()
.into();
assert_eq!(json.kind(), DbErrorKind::Data);
}
#[test]
fn retry_and_closed_stay_distinguishable() {
assert_ne!(
DbError::BufferEmpty.kind(),
DbError::BufferClosed {
buffer_name: "b".to_string()
}
.kind()
);
}
}