use std::error::Error as StdError;
use std::fmt;
#[derive(Debug, Clone)]
pub struct Error {
pub category: ErrorCategory,
pub code: ErrorCode,
pub message: String,
pub cause: Option<Box<Error>>,
pub location: Option<ErrorLocation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Core,
Dsp,
Graph,
Io,
Control,
Config,
Runtime,
Internal,
}
impl ErrorCategory {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCategory::Core => "core",
ErrorCategory::Dsp => "dsp",
ErrorCategory::Graph => "graph",
ErrorCategory::Io => "io",
ErrorCategory::Control => "control",
ErrorCategory::Config => "config",
ErrorCategory::Runtime => "runtime",
ErrorCategory::Internal => "internal",
}
}
}
impl fmt::Display for ErrorCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
Unknown = 0,
InvalidParameter = 1,
InvalidState = 2,
Unsupported = 3,
NotImplemented = 4,
Timeout = 5,
BufferFull = 100,
BufferEmpty = 101,
InvalidBufferSize = 102,
BufferMisaligned = 103,
BufferNotInitialized = 104,
QueueFull = 120,
QueueEmpty = 121,
QueueClosed = 122,
InvalidQueueIndex = 123,
NodeNotFound = 200,
PortNotFound = 201,
InvalidConnection = 202,
CycleDetected = 203,
NodeAlreadyExists = 204,
PortAlreadyConnected = 205,
DeviceNotFound = 300,
DeviceBusy = 301,
AlsaError = 310,
JackError = 311,
PipeWireError = 312,
XRun = 320,
MidiError = 400,
OscError = 401,
MappingNotFound = 402,
AutomatonNotFound = 403,
InvalidParameterValue = 404,
ConfigNotFound = 500,
InvalidConfigFormat = 501,
MissingField = 502,
RealtimeViolation = 600,
PriorityError = 601,
AlreadyRunning = 602,
NotRunning = 603,
}
impl ErrorCode {
pub fn category(&self) -> ErrorCategory {
match *self {
ErrorCode::Unknown
| ErrorCode::InvalidParameter
| ErrorCode::InvalidState
| ErrorCode::Unsupported
| ErrorCode::NotImplemented
| ErrorCode::Timeout
| ErrorCode::BufferFull
| ErrorCode::BufferEmpty
| ErrorCode::InvalidBufferSize
| ErrorCode::BufferMisaligned
| ErrorCode::BufferNotInitialized
| ErrorCode::QueueFull
| ErrorCode::QueueEmpty
| ErrorCode::QueueClosed
| ErrorCode::InvalidQueueIndex => ErrorCategory::Core,
ErrorCode::NodeNotFound
| ErrorCode::PortNotFound
| ErrorCode::InvalidConnection
| ErrorCode::CycleDetected
| ErrorCode::NodeAlreadyExists
| ErrorCode::PortAlreadyConnected => ErrorCategory::Graph,
ErrorCode::DeviceNotFound
| ErrorCode::DeviceBusy
| ErrorCode::AlsaError
| ErrorCode::JackError
| ErrorCode::PipeWireError
| ErrorCode::XRun => ErrorCategory::Io,
ErrorCode::MidiError
| ErrorCode::OscError
| ErrorCode::MappingNotFound
| ErrorCode::AutomatonNotFound
| ErrorCode::InvalidParameterValue => ErrorCategory::Control,
ErrorCode::ConfigNotFound
| ErrorCode::InvalidConfigFormat
| ErrorCode::MissingField => ErrorCategory::Config,
ErrorCode::RealtimeViolation
| ErrorCode::PriorityError
| ErrorCode::AlreadyRunning
| ErrorCode::NotRunning => ErrorCategory::Runtime,
}
}
pub fn description(&self) -> &'static str {
match self {
ErrorCode::Unknown => "Unknown error",
ErrorCode::InvalidParameter => "Invalid parameter",
ErrorCode::InvalidState => "Invalid state",
ErrorCode::Unsupported => "Unsupported operation",
ErrorCode::NotImplemented => "Not implemented",
ErrorCode::Timeout => "Operation timed out",
ErrorCode::BufferFull => "Buffer is full",
ErrorCode::BufferEmpty => "Buffer is empty",
ErrorCode::InvalidBufferSize => "Invalid buffer size",
ErrorCode::BufferMisaligned => "Buffer is misaligned for SIMD operations",
ErrorCode::BufferNotInitialized => "Buffer not initialized",
ErrorCode::QueueFull => "Queue is full",
ErrorCode::QueueEmpty => "Queue is empty",
ErrorCode::QueueClosed => "Queue is closed",
ErrorCode::InvalidQueueIndex => "Invalid queue index",
ErrorCode::NodeNotFound => "Node not found",
ErrorCode::PortNotFound => "Port not found",
ErrorCode::InvalidConnection => "Invalid connection",
ErrorCode::CycleDetected => "Cycle detected in graph",
ErrorCode::NodeAlreadyExists => "Node already exists",
ErrorCode::PortAlreadyConnected => "Port already connected",
ErrorCode::DeviceNotFound => "Device not found",
ErrorCode::DeviceBusy => "Device is busy",
ErrorCode::AlsaError => "ALSA error",
ErrorCode::JackError => "JACK error",
ErrorCode::PipeWireError => "PipeWire error",
ErrorCode::XRun => "Buffer underrun/overrun detected",
ErrorCode::MidiError => "MIDI error",
ErrorCode::OscError => "OSC error",
ErrorCode::MappingNotFound => "Mapping not found",
ErrorCode::AutomatonNotFound => "Automaton not found",
ErrorCode::InvalidParameterValue => "Invalid parameter value",
ErrorCode::ConfigNotFound => "Configuration not found",
ErrorCode::InvalidConfigFormat => "Invalid configuration format",
ErrorCode::MissingField => "Missing required field",
ErrorCode::RealtimeViolation => "Real-time violation detected",
ErrorCode::PriorityError => "Failed to set thread priority",
ErrorCode::AlreadyRunning => "Already running",
ErrorCode::NotRunning => "Not running",
}
}
}
#[derive(Debug, Clone)]
pub struct ErrorLocation {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl fmt::Display for ErrorLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
impl Error {
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
category: code.category(),
code,
message: message.into(),
cause: None,
location: None,
}
}
pub fn with_cause(mut self, cause: Error) -> Self {
self.cause = Some(Box::new(cause));
self
}
pub fn at(mut self, file: &'static str, line: u32, column: u32) -> Self {
self.location = Some(ErrorLocation { file, line, column });
self
}
pub fn root_cause(&self) -> &Error {
let mut current = self;
while let Some(cause) = ¤t.cause {
current = cause;
}
current
}
pub fn is_realtime_critical(&self) -> bool {
matches!(
self.code,
ErrorCode::RealtimeViolation
| ErrorCode::PriorityError
| ErrorCode::BufferFull
| ErrorCode::XRun
)
}
pub fn is_recoverable(&self) -> bool {
!self.is_realtime_critical()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(loc) = &self.location {
write!(
f,
"[{}] at {}: {} ({})",
self.category,
loc,
self.message,
self.code.description()
)?;
} else {
write!(
f,
"[{}]: {} ({})",
self.category,
self.message,
self.code.description()
)?;
}
if let Some(cause) = &self.cause {
write!(f, "\n caused by: {}", cause)?;
}
Ok(())
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.cause.as_ref().map(|c| c as &dyn StdError)
}
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Self {
Error::new(ErrorCode::Unknown, err.to_string())
}
}
impl From<std::num::ParseIntError> for Error {
fn from(err: std::num::ParseIntError) -> Self {
Error::new(ErrorCode::InvalidParameter, err.to_string())
}
}
impl From<std::num::ParseFloatError> for Error {
fn from(err: std::num::ParseFloatError) -> Self {
Error::new(ErrorCode::InvalidParameter, err.to_string())
}
}
impl From<std::str::Utf8Error> for Error {
fn from(err: std::str::Utf8Error) -> Self {
Error::new(ErrorCode::InvalidParameter, err.to_string())
}
}
#[macro_export]
macro_rules! error {
($code:expr, $msg:expr) => {
$crate::error::Error::new($code, $msg)
};
($code:expr, $fmt:expr, $($arg:tt)*) => {
$crate::error::Error::new($code, format!($fmt, $($arg)*))
};
}
#[macro_export]
macro_rules! error_at {
($code:expr, $msg:expr) => {
$crate::error::Error::new($code, $msg).at(file!(), line!(), column!())
};
($code:expr, $fmt:expr, $($arg:tt)*) => {
$crate::error::Error::new($code, format!($fmt, $($arg)*))
.at(file!(), line!(), column!())
};
}
#[macro_export]
macro_rules! bail {
($code:expr, $msg:expr) => {
return Err($crate::error::Error::new($code, $msg))
};
($code:expr, $fmt:expr, $($arg:tt)*) => {
return Err($crate::error::Error::new($code, format!($fmt, $($arg)*)))
};
}
#[macro_export]
macro_rules! context {
($expr:expr, $code:expr, $msg:expr) => {
$expr.map_err(|e| $crate::error::Error::new($code, $msg).with_cause(e))
};
($expr:expr, $code:expr, $fmt:expr, $($arg:tt)*) => {
$expr.map_err(|e| $crate::error::Error::new($code, format!($fmt, $($arg)*)).with_cause(e))
};
}
pub mod io {
use super::*;
pub fn device_not_found(name: &str) -> Error {
error!(ErrorCode::DeviceNotFound, "Device not found: {}", name)
}
pub fn device_busy(name: &str) -> Error {
error!(ErrorCode::DeviceBusy, "Device is busy: {}", name)
}
pub fn alsa_error(desc: &str) -> Error {
error!(ErrorCode::AlsaError, "ALSA error: {}", desc)
}
pub fn jack_error(desc: &str) -> Error {
error!(ErrorCode::JackError, "JACK error: {}", desc)
}
pub fn pipewire_error(desc: &str) -> Error {
error!(ErrorCode::PipeWireError, "PipeWire error: {}", desc)
}
pub fn xrun() -> Error {
Error::new(ErrorCode::XRun, "Buffer underrun/overrun detected")
}
}
pub mod control {
use super::*;
pub fn midi_error(desc: &str) -> Error {
error!(ErrorCode::MidiError, "MIDI error: {}", desc)
}
pub fn osc_error(desc: &str) -> Error {
error!(ErrorCode::OscError, "OSC error: {}", desc)
}
pub fn mapping_not_found(id: &str) -> Error {
error!(ErrorCode::MappingNotFound, "Mapping not found: {}", id)
}
pub fn automaton_not_found(id: &str) -> Error {
error!(ErrorCode::AutomatonNotFound, "Automaton not found: {}", id)
}
pub fn invalid_parameter_value(param: &str, value: f64, min: f64, max: f64) -> Error {
error!(
ErrorCode::InvalidParameterValue,
"Invalid value for parameter {}: {} (allowed range: {} - {})", param, value, min, max
)
}
}
pub mod config {
use super::*;
pub fn not_found(path: &str) -> Error {
error!(
ErrorCode::ConfigNotFound,
"Configuration not found: {}", path
)
}
pub fn invalid_format(details: &str) -> Error {
error!(
ErrorCode::InvalidConfigFormat,
"Invalid configuration format: {}", details
)
}
pub fn missing_field(field: &str) -> Error {
error!(ErrorCode::MissingField, "Missing required field: {}", field)
}
}
pub mod runtime {
use super::*;
pub fn realtime_violation(details: &str) -> Error {
error!(
ErrorCode::RealtimeViolation,
"Real-time violation: {}", details
)
}
pub fn priority_error(details: &str) -> Error {
error!(
ErrorCode::PriorityError,
"Failed to set thread priority: {}", details
)
}
pub fn already_running() -> Error {
Error::new(ErrorCode::AlreadyRunning, "Already running")
}
pub fn not_running() -> Error {
Error::new(ErrorCode::NotRunning, "Not running")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = Error::new(ErrorCode::BufferFull, "Test error");
assert_eq!(err.code, ErrorCode::BufferFull);
assert_eq!(err.message, "Test error");
assert_eq!(err.category, ErrorCategory::Core);
}
#[test]
fn test_error_with_cause() {
let cause = Error::new(ErrorCode::BufferEmpty, "Cause");
let err = Error::new(ErrorCode::BufferFull, "Main error").with_cause(cause);
assert!(err.cause.is_some());
assert_eq!(err.root_cause().code, ErrorCode::BufferEmpty);
}
#[test]
fn test_error_macros() {
let err = error!(ErrorCode::BufferFull, "Buffer is full");
assert_eq!(err.code, ErrorCode::BufferFull);
let err = error!(ErrorCode::BufferFull, "Buffer {} is full", "test");
assert_eq!(err.message, "Buffer test is full");
}
#[test]
fn test_specialized_errors() {
let err = io::device_not_found("hw:0");
assert_eq!(err.code, ErrorCode::DeviceNotFound);
assert!(err.message.contains("hw:0"));
}
#[test]
fn test_error_category() {
assert_eq!(ErrorCode::BufferFull.category(), ErrorCategory::Core);
assert_eq!(ErrorCode::NodeNotFound.category(), ErrorCategory::Graph);
assert_eq!(ErrorCode::AlsaError.category(), ErrorCategory::Io);
assert_eq!(ErrorCode::MidiError.category(), ErrorCategory::Control);
assert_eq!(ErrorCode::ConfigNotFound.category(), ErrorCategory::Config);
assert_eq!(
ErrorCode::RealtimeViolation.category(),
ErrorCategory::Runtime
);
}
#[test]
fn test_realtime_critical() {
assert!(io::xrun().is_realtime_critical());
assert!(runtime::realtime_violation("test").is_realtime_critical());
assert!(!config::not_found("test").is_realtime_critical());
}
}