use std::fmt;
use thiserror::Error;
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ProcessError {
#[error("Processing error: {0}")]
Processing(String),
#[error("Parameter error: {0}")]
Parameter(String),
#[error("Invalid port: {0}")]
InvalidPort(String),
#[error("Buffer error: {0}")]
Buffer(String),
#[error("Node {0} not found")]
NodeNotFound(u32),
#[error("Port {0} not found")]
PortNotFound(String),
#[error("Connection error: {0}")]
Connection(String),
#[error("Type mismatch: expected {expected}, got {got}")]
TypeMismatch {
expected: &'static str,
got: &'static str,
},
#[error("Sample rate mismatch: expected {expected}, got {got}")]
SampleRateMismatch {
expected: f32,
got: f32,
},
#[error("Configuration error: {0}")]
Config(String),
#[error("Not initialized")]
NotInitialized,
#[error("Already initialized")]
AlreadyInitialized,
#[error("Unsupported operation: {0}")]
Unsupported(String),
#[error("Operation timed out")]
Timeout,
#[error("Realtime violation: {0}")]
RealtimeViolation(String),
#[error("Internal error: {0}")]
Internal(String),
}
pub type ProcessResult<T> = Result<T, ProcessError>;
impl ProcessError {
pub fn processing(msg: impl Into<String>) -> Self {
Self::Processing(msg.into())
}
pub fn parameter(msg: impl Into<String>) -> Self {
Self::Parameter(msg.into())
}
pub fn invalid_port(port: impl fmt::Display) -> Self {
Self::InvalidPort(format!("Invalid port: {}", port))
}
pub fn buffer(msg: impl Into<String>) -> Self {
Self::Buffer(msg.into())
}
pub fn node_not_found(id: u32) -> Self {
Self::NodeNotFound(id)
}
pub fn port_not_found(port: impl fmt::Display) -> Self {
Self::PortNotFound(format!("{}", port))
}
pub fn connection(msg: impl Into<String>) -> Self {
Self::Connection(msg.into())
}
pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
Self::TypeMismatch { expected, got }
}
pub fn sample_rate_mismatch(expected: f32, got: f32) -> Self {
Self::SampleRateMismatch { expected, got }
}
pub fn config(msg: impl Into<String>) -> Self {
Self::Config(msg.into())
}
pub fn unsupported(msg: impl Into<String>) -> Self {
Self::Unsupported(msg.into())
}
pub fn internal(msg: impl Into<String>) -> Self {
Self::Internal(msg.into())
}
pub fn is_recoverable(&self) -> bool {
match self {
Self::Processing(_) => true,
Self::Parameter(_) => true,
Self::InvalidPort(_) => false,
Self::Buffer(_) => true,
Self::NodeNotFound(_) => false,
Self::PortNotFound(_) => false,
Self::Connection(_) => false,
Self::TypeMismatch { .. } => false,
Self::SampleRateMismatch { .. } => false,
Self::Config(_) => false,
Self::NotInitialized => true,
Self::AlreadyInitialized => true,
Self::Unsupported(_) => false,
Self::Timeout => true,
Self::RealtimeViolation(_) => false,
Self::Internal(_) => false,
}
}
pub fn code(&self) -> &'static str {
match self {
Self::Processing(_) => "ERR_PROCESSING",
Self::Parameter(_) => "ERR_PARAMETER",
Self::InvalidPort(_) => "ERR_INVALID_PORT",
Self::Buffer(_) => "ERR_BUFFER",
Self::NodeNotFound(_) => "ERR_NODE_NOT_FOUND",
Self::PortNotFound(_) => "ERR_PORT_NOT_FOUND",
Self::Connection(_) => "ERR_CONNECTION",
Self::TypeMismatch { .. } => "ERR_TYPE_MISMATCH",
Self::SampleRateMismatch { .. } => "ERR_SAMPLE_RATE",
Self::Config(_) => "ERR_CONFIG",
Self::NotInitialized => "ERR_NOT_INIT",
Self::AlreadyInitialized => "ERR_ALREADY_INIT",
Self::Unsupported(_) => "ERR_UNSUPPORTED",
Self::Timeout => "ERR_TIMEOUT",
Self::RealtimeViolation(_) => "ERR_RT_VIOLATION",
Self::Internal(_) => "ERR_INTERNAL",
}
}
}
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ParameterError {
#[error("Parameter name cannot be empty")]
Empty,
#[error("Parameter name cannot contain '{0}'")]
InvalidCharacter(char),
#[error("Parameter name too long (max {max} characters)")]
TooLong {
max: usize,
},
#[error("Parameter name must start with a letter")]
MustStartWithLetter,
#[error("Parameter '{0}' not found")]
NotFound(String),
#[error("Parameter type mismatch: expected {expected:?}, got {got:?}")]
TypeMismatch {
expected: crate::traits::ParamType,
got: crate::traits::ParamType,
},
#[error("Value {value} out of range [{min}, {max}]")]
OutOfRange {
value: f32,
min: f32,
max: f32,
},
#[error("Invalid choice '{0}'")]
InvalidChoice(String),
#[error("Parameter '{0}' already exists")]
Duplicate(String),
#[error("Parameter '{0}' is read-only")]
ReadOnly(String),
}
pub type ParameterResult<T> = Result<T, ParameterError>;
impl ParameterError {
pub fn not_found(name: impl Into<String>) -> Self {
Self::NotFound(name.into())
}
pub fn type_mismatch(
expected: crate::traits::ParamType,
got: crate::traits::ParamType,
) -> Self {
Self::TypeMismatch { expected, got }
}
pub fn out_of_range(value: f32, min: f32, max: f32) -> Self {
Self::OutOfRange { value, min, max }
}
pub fn invalid_choice(choice: impl Into<String>) -> Self {
Self::InvalidChoice(choice.into())
}
pub fn duplicate(name: impl Into<String>) -> Self {
Self::Duplicate(name.into())
}
pub fn read_only(name: impl Into<String>) -> Self {
Self::ReadOnly(name.into())
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum PortError {
#[error("Port {0} not found")]
NotFound(String),
#[error("Port direction mismatch: expected {expected}, got {got}")]
DirectionMismatch {
expected: crate::traits::PortDirection,
got: crate::traits::PortDirection,
},
#[error("Port type mismatch: expected {expected:?}, got {got:?}")]
TypeMismatch {
expected: crate::traits::PortType,
got: crate::traits::PortType,
},
#[error("Port {0} is already connected")]
AlreadyConnected(String),
#[error("Maximum connections reached for port {0}")]
MaxConnectionsReached(String),
#[error("Invalid port index: {0}")]
InvalidIndex(usize),
}
pub type PortResult<T> = Result<T, PortError>;
impl PortError {
pub fn not_found(port: impl fmt::Display) -> Self {
Self::NotFound(format!("{}", port))
}
pub fn direction_mismatch(
expected: crate::traits::PortDirection,
got: crate::traits::PortDirection,
) -> Self {
Self::DirectionMismatch { expected, got }
}
pub fn type_mismatch(expected: crate::traits::PortType, got: crate::traits::PortType) -> Self {
Self::TypeMismatch { expected, got }
}
pub fn already_connected(port: impl fmt::Display) -> Self {
Self::AlreadyConnected(format!("{}", port))
}
}
#[derive(Error, Debug, Clone, PartialEq)]
pub enum ClockError {
#[error("Hardware error: {0}")]
Hardware(String),
#[error("Invalid sample rate: {0}")]
InvalidSampleRate(f32),
#[error("Clock not started")]
NotStarted,
#[error("Clock already started")]
AlreadyStarted,
#[error("Clock underflow")]
Underflow,
#[error("Clock overflow")]
Overflow,
}
pub type ClockResult<T> = Result<T, ClockError>;
#[derive(Error, Debug, Clone, PartialEq, Eq)]
pub enum ConnectionError {
#[error("Cannot connect node to itself")]
SelfConnection,
#[error("Cycle detected in graph")]
CycleDetected,
#[error("Connection would create a cycle")]
WouldCreateCycle,
#[error("Invalid connection: {0}")]
Invalid(String),
}
pub type ConnectionResult<T> = Result<T, ConnectionError>;
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub location: Option<String>,
pub thread_id: Option<std::thread::ThreadId>,
pub timestamp: std::time::SystemTime,
pub node_id: Option<crate::traits::NodeId>,
pub port_id: Option<String>,
pub parameter_id: Option<String>,
pub extras: Vec<(String, String)>,
}
impl Default for ErrorContext {
fn default() -> Self {
Self {
location: None,
thread_id: Some(std::thread::current().id()),
timestamp: std::time::SystemTime::now(),
node_id: None,
port_id: None,
parameter_id: None,
extras: Vec::new(),
}
}
}
impl ErrorContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_location(mut self, file: &str, line: u32) -> Self {
self.location = Some(format!("{}:{}", file, line));
self
}
pub fn with_node(mut self, node_id: crate::traits::NodeId) -> Self {
self.node_id = Some(node_id);
self
}
pub fn with_port(mut self, port_id: impl fmt::Display) -> Self {
self.port_id = Some(format!("{}", port_id));
self
}
pub fn with_parameter(mut self, param_id: impl AsRef<str>) -> Self {
self.parameter_id = Some(param_id.as_ref().to_string());
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extras.push((key.into(), value.into()));
self
}
pub fn format(&self, error: &impl std::error::Error) -> String {
let mut msg = format!("{}", error);
if let Some(loc) = &self.location {
msg.push_str(&format!("\n at {}", loc));
}
if let Some(id) = self.thread_id {
msg.push_str(&format!("\n thread: {:?}", id));
}
if let Some(node) = self.node_id {
msg.push_str(&format!("\n node: {}", node));
}
if let Some(port) = &self.port_id {
msg.push_str(&format!("\n port: {}", port));
}
if let Some(param) = &self.parameter_id {
msg.push_str(&format!("\n parameter: {}", param));
}
for (key, value) in &self.extras {
msg.push_str(&format!("\n {}: {}", key, value));
}
msg
}
}
impl From<ParameterError> for ProcessError {
fn from(err: ParameterError) -> Self {
match err {
ParameterError::NotFound(name) => {
Self::parameter(format!("Parameter not found: {}", name))
}
ParameterError::TypeMismatch { expected, got } => {
Self::type_mismatch(expected.name(), got.name())
}
ParameterError::OutOfRange { value, min, max } => {
Self::parameter(format!("Value {} out of range [{}, {}]", value, min, max))
}
ParameterError::InvalidChoice(choice) => {
Self::parameter(format!("Invalid choice: {}", choice))
}
ParameterError::Duplicate(name) => {
Self::parameter(format!("Duplicate parameter: {}", name))
}
ParameterError::ReadOnly(name) => {
Self::parameter(format!("Parameter is read-only: {}", name))
}
_ => Self::parameter(err.to_string()),
}
}
}
impl From<PortError> for ProcessError {
fn from(err: PortError) -> Self {
match err {
PortError::NotFound(port) => Self::port_not_found(port),
PortError::DirectionMismatch { expected, got } => {
Self::type_mismatch(expected.name(), got.name())
}
PortError::TypeMismatch { expected, got } => {
Self::type_mismatch(expected.name(), got.name())
}
PortError::AlreadyConnected(port) => {
Self::connection(format!("Port already connected: {}", port))
}
PortError::MaxConnectionsReached(port) => {
Self::connection(format!("Max connections reached for port: {}", port))
}
PortError::InvalidIndex(idx) => {
Self::invalid_port(format!("Invalid port index: {}", idx))
}
}
}
}
impl From<ClockError> for ProcessError {
fn from(err: ClockError) -> Self {
match err {
ClockError::Hardware(msg) => Self::processing(format!("Hardware error: {}", msg)),
ClockError::InvalidSampleRate(sr) => {
Self::config(format!("Invalid sample rate: {}", sr))
}
ClockError::NotStarted => Self::processing("Clock not started"),
ClockError::AlreadyStarted => Self::processing("Clock already started"),
ClockError::Underflow => Self::buffer("Clock underflow"),
ClockError::Overflow => Self::buffer("Clock overflow"),
}
}
}
impl From<ConnectionError> for ProcessError {
fn from(err: ConnectionError) -> Self {
match err {
ConnectionError::SelfConnection => Self::connection("Cannot connect node to itself"),
ConnectionError::CycleDetected => Self::connection("Cycle detected in graph"),
ConnectionError::WouldCreateCycle => {
Self::connection("Connection would create a cycle")
}
ConnectionError::Invalid(msg) => Self::connection(msg),
}
}
}
impl From<std::io::Error> for ProcessError {
fn from(err: std::io::Error) -> Self {
Self::Processing(format!("IO error: {}", err))
}
}
impl From<crate::error::Error> for ProcessError {
fn from(err: crate::error::Error) -> Self {
Self::Processing(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::{PortDirection, PortType};
#[test]
fn test_process_error_creation() {
let err = ProcessError::processing("test error");
assert!(matches!(err, ProcessError::Processing(_)));
assert_eq!(err.code(), "ERR_PROCESSING");
assert!(err.is_recoverable());
let err = ProcessError::node_not_found(42);
assert!(matches!(err, ProcessError::NodeNotFound(42)));
assert_eq!(err.code(), "ERR_NODE_NOT_FOUND");
assert!(!err.is_recoverable());
}
#[test]
fn test_parameter_error_creation() {
let err = ParameterError::not_found("gain");
assert!(matches!(err, ParameterError::NotFound(_)));
let err = ParameterError::out_of_range(2.0, 0.0, 1.0);
assert!(matches!(err, ParameterError::OutOfRange { value: 2.0, .. }));
}
#[test]
fn test_port_error_creation() {
let err = PortError::direction_mismatch(PortDirection::Input, PortDirection::Output);
assert!(matches!(err, PortError::DirectionMismatch { .. }));
let err = PortError::type_mismatch(PortType::Signal, PortType::Control);
assert!(matches!(err, PortError::TypeMismatch { .. }));
}
#[test]
fn test_error_conversions() {
let param_err = ParameterError::not_found("test");
let proc_err: ProcessError = param_err.into();
assert!(matches!(proc_err, ProcessError::Parameter(_)));
let port_err = PortError::not_found("port");
let proc_err: ProcessError = port_err.into();
assert!(matches!(proc_err, ProcessError::PortNotFound(_)));
let clock_err = ClockError::Underflow;
let proc_err: ProcessError = clock_err.into();
assert!(matches!(proc_err, ProcessError::Buffer(_)));
}
#[test]
fn test_error_context() {
let ctx = ErrorContext::new()
.with_location("test.rs", 42)
.with_node(crate::traits::NodeId(1))
.with_extra("sample_rate", "44100");
let err = ProcessError::processing("test");
let formatted = ctx.format(&err);
assert!(formatted.contains("test.rs:42"));
assert!(formatted.contains("node: Node(1)"));
assert!(formatted.contains("sample_rate: 44100"));
}
#[test]
fn test_recoverable_flags() {
assert!(ProcessError::processing("test").is_recoverable());
assert!(ProcessError::parameter("test").is_recoverable());
assert!(ProcessError::buffer("test").is_recoverable());
assert!(!ProcessError::node_not_found(42).is_recoverable());
assert!(!ProcessError::port_not_found("port").is_recoverable());
}
#[test]
fn test_error_codes() {
assert_eq!(ProcessError::processing("").code(), "ERR_PROCESSING");
assert_eq!(ProcessError::node_not_found(0).code(), "ERR_NODE_NOT_FOUND");
}
#[test]
fn test_parameter_error_details() {
let err = ParameterError::out_of_range(1.5, 0.0, 1.0);
match err {
ParameterError::OutOfRange { value, min, max } => {
assert_eq!(value, 1.5);
assert_eq!(min, 0.0);
assert_eq!(max, 1.0);
}
_ => panic!("Wrong error type"),
}
}
}