Skip to main content

rill_core/traits/
error.rs

1//! # Error Types for Rill Traits
2//!
3//! This module defines the error types used throughout the Rill ecosystem.
4//! All errors implement `std::error::Error` and are designed to be:
5//! - Thread-safe (`Send + Sync`)
6//! - Cloneable for passing between threads
7//! - Human-readable with detailed context
8//! - Real-time safe (no allocations in error paths)
9
10use std::fmt;
11use thiserror::Error;
12
13// ============================================================================
14// Core Process Error
15// ============================================================================
16
17/// Main error type for audio processing operations
18///
19/// This error can occur during node processing, parameter changes,
20/// or any other operation in the audio graph.
21#[derive(Error, Debug, Clone, PartialEq)]
22pub enum ProcessError {
23    /// Error during audio processing
24    #[error("Processing error: {0}")]
25    Processing(String),
26
27    /// Error with a parameter (invalid value, out of range, etc.)
28    #[error("Parameter error: {0}")]
29    Parameter(String),
30
31    /// Invalid port access
32    #[error("Invalid port: {0}")]
33    InvalidPort(String),
34
35    /// Buffer operation failed
36    #[error("Buffer error: {0}")]
37    Buffer(String),
38
39    /// Node not found
40    #[error("Node {0} not found")]
41    NodeNotFound(u32),
42
43    /// Port not found
44    #[error("Port {0} not found")]
45    PortNotFound(String),
46
47    /// Connection error
48    #[error("Connection error: {0}")]
49    Connection(String),
50
51    /// Type mismatch (e.g., trying to connect audio to control)
52    #[error("Type mismatch: expected {expected}, got {got}")]
53    TypeMismatch {
54        /// Expected type
55        expected: &'static str,
56        /// Actual type
57        got: &'static str,
58    },
59
60    /// Sample rate mismatch
61    #[error("Sample rate mismatch: expected {expected}, got {got}")]
62    SampleRateMismatch {
63        /// Expected sample rate
64        expected: f32,
65        /// Actual sample rate
66        got: f32,
67    },
68
69    /// Configuration error
70    #[error("Configuration error: {0}")]
71    Config(String),
72
73    /// Not initialized
74    #[error("Not initialized")]
75    NotInitialized,
76
77    /// Already initialized
78    #[error("Already initialized")]
79    AlreadyInitialized,
80
81    /// Unsupported operation
82    #[error("Unsupported operation: {0}")]
83    Unsupported(String),
84
85    /// Timeout occurred
86    #[error("Operation timed out")]
87    Timeout,
88
89    /// Internal error (for implementation-specific errors)
90    #[error("Internal error: {0}")]
91    Internal(String),
92}
93
94/// Result type for audio processing operations
95pub type ProcessResult<T> = Result<T, ProcessError>;
96
97impl ProcessError {
98    /// Create a new processing error with a formatted message
99    pub fn processing(msg: impl Into<String>) -> Self {
100        Self::Processing(msg.into())
101    }
102
103    /// Create a new parameter error with a formatted message
104    pub fn parameter(msg: impl Into<String>) -> Self {
105        Self::Parameter(msg.into())
106    }
107
108    /// Create a new invalid port error
109    pub fn invalid_port(port: impl fmt::Display) -> Self {
110        Self::InvalidPort(format!("Invalid port: {}", port))
111    }
112
113    /// Create a new buffer error
114    pub fn buffer(msg: impl Into<String>) -> Self {
115        Self::Buffer(msg.into())
116    }
117
118    /// Create a new node not found error
119    pub fn node_not_found(id: u32) -> Self {
120        Self::NodeNotFound(id)
121    }
122
123    /// Create a new port not found error
124    pub fn port_not_found(port: impl fmt::Display) -> Self {
125        Self::PortNotFound(format!("{}", port))
126    }
127
128    /// Create a new connection error
129    pub fn connection(msg: impl Into<String>) -> Self {
130        Self::Connection(msg.into())
131    }
132
133    /// Create a new type mismatch error
134    pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
135        Self::TypeMismatch { expected, got }
136    }
137
138    /// Create a new sample rate mismatch error
139    pub fn sample_rate_mismatch(expected: f32, got: f32) -> Self {
140        Self::SampleRateMismatch { expected, got }
141    }
142
143    /// Create a new configuration error
144    pub fn config(msg: impl Into<String>) -> Self {
145        Self::Config(msg.into())
146    }
147
148    /// Create a new unsupported operation error
149    pub fn unsupported(msg: impl Into<String>) -> Self {
150        Self::Unsupported(msg.into())
151    }
152
153    /// Create a new internal error
154    pub fn internal(msg: impl Into<String>) -> Self {
155        Self::Internal(msg.into())
156    }
157
158    /// Check if this error is recoverable
159    ///
160    /// Recoverable errors are those that don't require stopping the audio thread,
161    /// such as temporary buffer underflows or parameter errors.
162    pub fn is_recoverable(&self) -> bool {
163        match self {
164            Self::Processing(_) => true,
165            Self::Parameter(_) => true,
166            Self::InvalidPort(_) => false,
167            Self::Buffer(_) => true,
168            Self::NodeNotFound(_) => false,
169            Self::PortNotFound(_) => false,
170            Self::Connection(_) => false,
171            Self::TypeMismatch { .. } => false,
172            Self::SampleRateMismatch { .. } => false,
173            Self::Config(_) => false,
174            Self::NotInitialized => true,
175            Self::AlreadyInitialized => true,
176            Self::Unsupported(_) => false,
177            Self::Timeout => true,
178            Self::Internal(_) => false,
179        }
180    }
181
182    /// Get a short error code for this error (useful for logging)
183    pub fn code(&self) -> &'static str {
184        match self {
185            Self::Processing(_) => "ERR_PROCESSING",
186            Self::Parameter(_) => "ERR_PARAMETER",
187            Self::InvalidPort(_) => "ERR_INVALID_PORT",
188            Self::Buffer(_) => "ERR_BUFFER",
189            Self::NodeNotFound(_) => "ERR_NODE_NOT_FOUND",
190            Self::PortNotFound(_) => "ERR_PORT_NOT_FOUND",
191            Self::Connection(_) => "ERR_CONNECTION",
192            Self::TypeMismatch { .. } => "ERR_TYPE_MISMATCH",
193            Self::SampleRateMismatch { .. } => "ERR_SAMPLE_RATE",
194            Self::Config(_) => "ERR_CONFIG",
195            Self::NotInitialized => "ERR_NOT_INIT",
196            Self::AlreadyInitialized => "ERR_ALREADY_INIT",
197            Self::Unsupported(_) => "ERR_UNSUPPORTED",
198            Self::Timeout => "ERR_TIMEOUT",
199            Self::Internal(_) => "ERR_INTERNAL",
200        }
201    }
202}
203
204// ============================================================================
205// Parameter Error
206// ============================================================================
207
208/// Errors that can occur during parameter operations
209#[derive(Error, Debug, Clone, PartialEq)]
210pub enum ParameterError {
211    /// Parameter name is empty
212    #[error("Parameter name cannot be empty")]
213    Empty,
214
215    /// Parameter name contains invalid character
216    #[error("Parameter name cannot contain '{0}'")]
217    InvalidCharacter(char),
218
219    /// Parameter name is too long
220    #[error("Parameter name too long (max {max} characters)")]
221    TooLong {
222        /// Maximum allowed length
223        max: usize,
224    },
225
226    /// Parameter name must start with a letter
227    #[error("Parameter name must start with a letter")]
228    MustStartWithLetter,
229
230    /// Parameter not found
231    #[error("Parameter '{0}' not found")]
232    NotFound(String),
233
234    /// Parameter type mismatch
235    #[error("Parameter type mismatch: expected {expected:?}, got {got:?}")]
236    TypeMismatch {
237        /// Expected parameter type
238        expected: crate::traits::ParamType,
239        /// Actual parameter type
240        got: crate::traits::ParamType,
241    },
242
243    /// Value out of range
244    #[error("Value {value} out of range [{min}, {max}]")]
245    OutOfRange {
246        /// The value that was out of range
247        value: f32,
248        /// Minimum allowed value
249        min: f32,
250        /// Maximum allowed value
251        max: f32,
252    },
253
254    /// Invalid choice (for Choice parameters)
255    #[error("Invalid choice '{0}'")]
256    InvalidChoice(String),
257
258    /// Duplicate parameter
259    #[error("Parameter '{0}' already exists")]
260    Duplicate(String),
261
262    /// Parameter is read-only
263    #[error("Parameter '{0}' is read-only")]
264    ReadOnly(String),
265}
266
267/// Result type for parameter operations
268pub type ParameterResult<T> = Result<T, ParameterError>;
269
270impl ParameterError {
271    /// Create a new not found error
272    pub fn not_found(name: impl Into<String>) -> Self {
273        Self::NotFound(name.into())
274    }
275
276    /// Create a new type mismatch error
277    pub fn type_mismatch(
278        expected: crate::traits::ParamType,
279        got: crate::traits::ParamType,
280    ) -> Self {
281        Self::TypeMismatch { expected, got }
282    }
283
284    /// Create a new out of range error
285    pub fn out_of_range(value: f32, min: f32, max: f32) -> Self {
286        Self::OutOfRange { value, min, max }
287    }
288
289    /// Create a new invalid choice error
290    pub fn invalid_choice(choice: impl Into<String>) -> Self {
291        Self::InvalidChoice(choice.into())
292    }
293
294    /// Create a new duplicate parameter error
295    pub fn duplicate(name: impl Into<String>) -> Self {
296        Self::Duplicate(name.into())
297    }
298
299    /// Create a new read-only error
300    pub fn read_only(name: impl Into<String>) -> Self {
301        Self::ReadOnly(name.into())
302    }
303}
304
305// ============================================================================
306// Port Error
307// ============================================================================
308
309/// Errors that can occur during port operations
310#[derive(Error, Debug, Clone, PartialEq, Eq)]
311pub enum PortError {
312    /// Port not found
313    #[error("Port {0} not found")]
314    NotFound(String),
315
316    /// Port direction mismatch (e.g., trying to connect output to output)
317    #[error("Port direction mismatch: expected {expected}, got {got}")]
318    DirectionMismatch {
319        /// Expected direction
320        expected: crate::traits::PortDirection,
321        /// Actual direction
322        got: crate::traits::PortDirection,
323    },
324
325    /// Port type mismatch (e.g., trying to connect audio to control)
326    #[error("Port type mismatch: expected {expected:?}, got {got:?}")]
327    TypeMismatch {
328        /// Expected port type
329        expected: crate::traits::PortType,
330        /// Actual port type
331        got: crate::traits::PortType,
332    },
333
334    /// Port already connected
335    #[error("Port {0} is already connected")]
336    AlreadyConnected(String),
337
338    /// Maximum connections reached
339    #[error("Maximum connections reached for port {0}")]
340    MaxConnectionsReached(String),
341
342    /// Invalid port index
343    #[error("Invalid port index: {0}")]
344    InvalidIndex(usize),
345}
346
347/// Result type for port operations
348pub type PortResult<T> = Result<T, PortError>;
349
350impl PortError {
351    /// Create a new not found error
352    pub fn not_found(port: impl fmt::Display) -> Self {
353        Self::NotFound(format!("{}", port))
354    }
355
356    /// Create a new direction mismatch error
357    pub fn direction_mismatch(
358        expected: crate::traits::PortDirection,
359        got: crate::traits::PortDirection,
360    ) -> Self {
361        Self::DirectionMismatch { expected, got }
362    }
363
364    /// Create a new type mismatch error
365    pub fn type_mismatch(expected: crate::traits::PortType, got: crate::traits::PortType) -> Self {
366        Self::TypeMismatch { expected, got }
367    }
368
369    /// Create a new already connected error
370    pub fn already_connected(port: impl fmt::Display) -> Self {
371        Self::AlreadyConnected(format!("{}", port))
372    }
373}
374
375// ============================================================================
376// Clock Error
377// ============================================================================
378
379/// Errors that can occur during clock operations
380#[derive(Error, Debug, Clone, PartialEq)]
381pub enum ClockError {
382    /// Hardware error (ALSA, JACK, etc.)
383    #[error("Hardware error: {0}")]
384    Hardware(String),
385
386    /// Invalid sample rate
387    #[error("Invalid sample rate: {0}")]
388    InvalidSampleRate(f32),
389
390    /// Clock not started
391    #[error("Clock not started")]
392    NotStarted,
393
394    /// Clock already started
395    #[error("Clock already started")]
396    AlreadyStarted,
397
398    /// Clock underflow
399    #[error("Clock underflow")]
400    Underflow,
401
402    /// Clock overflow
403    #[error("Clock overflow")]
404    Overflow,
405}
406
407/// Result type for clock operations
408pub type ClockResult<T> = Result<T, ClockError>;
409
410// ============================================================================
411// Connection Error
412// ============================================================================
413
414/// Errors that can occur during graph connections
415#[derive(Error, Debug, Clone, PartialEq, Eq)]
416pub enum ConnectionError {
417    /// Cannot connect node to itself
418    #[error("Cannot connect node to itself")]
419    SelfConnection,
420
421    /// Cycle detected in graph
422    #[error("Cycle detected in graph")]
423    CycleDetected,
424
425    /// Connection would create a cycle
426    #[error("Connection would create a cycle")]
427    WouldCreateCycle,
428
429    /// Invalid connection
430    #[error("Invalid connection: {0}")]
431    Invalid(String),
432}
433
434/// Result type for connection operations
435pub type ConnectionResult<T> = Result<T, ConnectionError>;
436
437// ============================================================================
438// Error Context (for adding extra information)
439// ============================================================================
440
441/// Additional context for errors
442///
443/// This can be attached to errors to provide more information
444/// about where and why they occurred.
445#[derive(Debug, Clone)]
446pub struct ErrorContext {
447    /// Source location (file:line)
448    pub location: Option<String>,
449
450    /// Thread ID where error occurred
451    pub thread_id: Option<std::thread::ThreadId>,
452
453    /// Timestamp when error occurred
454    pub timestamp: std::time::SystemTime,
455
456    /// Node ID (if applicable)
457    pub node_id: Option<crate::traits::NodeId>,
458
459    /// Port ID (if applicable)
460    pub port_id: Option<String>,
461
462    /// Parameter ID (if applicable)
463    pub parameter_id: Option<String>,
464
465    /// Additional key-value pairs
466    pub extras: Vec<(String, String)>,
467}
468
469impl Default for ErrorContext {
470    fn default() -> Self {
471        Self {
472            location: None,
473            thread_id: Some(std::thread::current().id()),
474            timestamp: std::time::SystemTime::now(),
475            node_id: None,
476            port_id: None,
477            parameter_id: None,
478            extras: Vec::new(),
479        }
480    }
481}
482
483impl ErrorContext {
484    /// Create new error context
485    pub fn new() -> Self {
486        Self::default()
487    }
488
489    /// Add source location
490    pub fn with_location(mut self, file: &str, line: u32) -> Self {
491        self.location = Some(format!("{}:{}", file, line));
492        self
493    }
494
495    /// Add node ID
496    pub fn with_node(mut self, node_id: crate::traits::NodeId) -> Self {
497        self.node_id = Some(node_id);
498        self
499    }
500
501    /// Add port ID
502    pub fn with_port(mut self, port_id: impl fmt::Display) -> Self {
503        self.port_id = Some(format!("{}", port_id));
504        self
505    }
506
507    /// Add parameter ID
508    pub fn with_parameter(mut self, param_id: impl AsRef<str>) -> Self {
509        self.parameter_id = Some(param_id.as_ref().to_string());
510        self
511    }
512
513    /// Add extra key-value pair
514    pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
515        self.extras.push((key.into(), value.into()));
516        self
517    }
518
519    /// Format error with context
520    pub fn format(&self, error: &impl std::error::Error) -> String {
521        let mut msg = format!("{}", error);
522
523        if let Some(loc) = &self.location {
524            msg.push_str(&format!("\n  at {}", loc));
525        }
526
527        if let Some(id) = self.thread_id {
528            msg.push_str(&format!("\n  thread: {:?}", id));
529        }
530
531        if let Some(node) = self.node_id {
532            msg.push_str(&format!("\n  node: {}", node));
533        }
534
535        if let Some(port) = &self.port_id {
536            msg.push_str(&format!("\n  port: {}", port));
537        }
538
539        if let Some(param) = &self.parameter_id {
540            msg.push_str(&format!("\n  parameter: {}", param));
541        }
542
543        for (key, value) in &self.extras {
544            msg.push_str(&format!("\n  {}: {}", key, value));
545        }
546
547        msg
548    }
549}
550
551// ============================================================================
552// Conversion Implementations
553// ============================================================================
554
555impl From<ParameterError> for ProcessError {
556    fn from(err: ParameterError) -> Self {
557        match err {
558            ParameterError::NotFound(name) => {
559                Self::parameter(format!("Parameter not found: {}", name))
560            }
561            ParameterError::TypeMismatch { expected, got } => {
562                Self::type_mismatch(expected.name(), got.name())
563            }
564            ParameterError::OutOfRange { value, min, max } => {
565                Self::parameter(format!("Value {} out of range [{}, {}]", value, min, max))
566            }
567            ParameterError::InvalidChoice(choice) => {
568                Self::parameter(format!("Invalid choice: {}", choice))
569            }
570            ParameterError::Duplicate(name) => {
571                Self::parameter(format!("Duplicate parameter: {}", name))
572            }
573            ParameterError::ReadOnly(name) => {
574                Self::parameter(format!("Parameter is read-only: {}", name))
575            }
576            _ => Self::parameter(err.to_string()),
577        }
578    }
579}
580
581impl From<PortError> for ProcessError {
582    fn from(err: PortError) -> Self {
583        match err {
584            PortError::NotFound(port) => Self::port_not_found(port),
585            PortError::DirectionMismatch { expected, got } => {
586                Self::type_mismatch(expected.name(), got.name())
587            }
588            PortError::TypeMismatch { expected, got } => {
589                Self::type_mismatch(expected.name(), got.name())
590            }
591            PortError::AlreadyConnected(port) => {
592                Self::connection(format!("Port already connected: {}", port))
593            }
594            PortError::MaxConnectionsReached(port) => {
595                Self::connection(format!("Max connections reached for port: {}", port))
596            }
597            PortError::InvalidIndex(idx) => {
598                Self::invalid_port(format!("Invalid port index: {}", idx))
599            }
600        }
601    }
602}
603
604impl From<ClockError> for ProcessError {
605    fn from(err: ClockError) -> Self {
606        match err {
607            ClockError::Hardware(msg) => Self::processing(format!("Hardware error: {}", msg)),
608            ClockError::InvalidSampleRate(sr) => {
609                Self::config(format!("Invalid sample rate: {}", sr))
610            }
611            ClockError::NotStarted => Self::processing("Clock not started"),
612            ClockError::AlreadyStarted => Self::processing("Clock already started"),
613            ClockError::Underflow => Self::buffer("Clock underflow"),
614            ClockError::Overflow => Self::buffer("Clock overflow"),
615        }
616    }
617}
618
619impl From<ConnectionError> for ProcessError {
620    fn from(err: ConnectionError) -> Self {
621        match err {
622            ConnectionError::SelfConnection => Self::connection("Cannot connect node to itself"),
623            ConnectionError::CycleDetected => Self::connection("Cycle detected in graph"),
624            ConnectionError::WouldCreateCycle => {
625                Self::connection("Connection would create a cycle")
626            }
627            ConnectionError::Invalid(msg) => Self::connection(msg),
628        }
629    }
630}
631
632impl From<std::io::Error> for ProcessError {
633    fn from(err: std::io::Error) -> Self {
634        Self::Processing(format!("IO error: {}", err))
635    }
636}
637
638impl From<crate::error::Error> for ProcessError {
639    fn from(err: crate::error::Error) -> Self {
640        Self::Processing(err.to_string())
641    }
642}
643
644// ============================================================================
645// Tests
646// ============================================================================
647
648#[cfg(test)]
649mod tests {
650    use super::*;
651    use crate::traits::{PortDirection, PortType};
652
653    #[test]
654    fn test_process_error_creation() {
655        let err = ProcessError::processing("test error");
656        assert!(matches!(err, ProcessError::Processing(_)));
657        assert_eq!(err.code(), "ERR_PROCESSING");
658        assert!(err.is_recoverable());
659
660        let err = ProcessError::node_not_found(42);
661        assert!(matches!(err, ProcessError::NodeNotFound(42)));
662        assert_eq!(err.code(), "ERR_NODE_NOT_FOUND");
663        assert!(!err.is_recoverable());
664    }
665
666    #[test]
667    fn test_parameter_error_creation() {
668        let err = ParameterError::not_found("gain");
669        assert!(matches!(err, ParameterError::NotFound(_)));
670
671        let err = ParameterError::out_of_range(2.0, 0.0, 1.0);
672        assert!(matches!(err, ParameterError::OutOfRange { value: 2.0, .. }));
673    }
674
675    #[test]
676    fn test_port_error_creation() {
677        let err = PortError::direction_mismatch(PortDirection::Input, PortDirection::Output);
678        assert!(matches!(err, PortError::DirectionMismatch { .. }));
679
680        let err = PortError::type_mismatch(PortType::Audio, PortType::Control);
681        assert!(matches!(err, PortError::TypeMismatch { .. }));
682    }
683
684    #[test]
685    fn test_error_conversions() {
686        let param_err = ParameterError::not_found("test");
687        let proc_err: ProcessError = param_err.into();
688        assert!(matches!(proc_err, ProcessError::Parameter(_)));
689
690        let port_err = PortError::not_found("port");
691        let proc_err: ProcessError = port_err.into();
692        assert!(matches!(proc_err, ProcessError::PortNotFound(_)));
693
694        let clock_err = ClockError::Underflow;
695        let proc_err: ProcessError = clock_err.into();
696        assert!(matches!(proc_err, ProcessError::Buffer(_)));
697    }
698
699    #[test]
700    fn test_error_context() {
701        let ctx = ErrorContext::new()
702            .with_location("test.rs", 42)
703            .with_node(crate::traits::NodeId(1))
704            .with_extra("sample_rate", "44100");
705
706        let err = ProcessError::processing("test");
707        let formatted = ctx.format(&err);
708
709        assert!(formatted.contains("test.rs:42"));
710        assert!(formatted.contains("node: Node(1)"));
711        assert!(formatted.contains("sample_rate: 44100"));
712    }
713
714    #[test]
715    fn test_recoverable_flags() {
716        assert!(ProcessError::processing("test").is_recoverable());
717        assert!(ProcessError::parameter("test").is_recoverable());
718        assert!(ProcessError::buffer("test").is_recoverable());
719        assert!(!ProcessError::node_not_found(42).is_recoverable());
720        assert!(!ProcessError::port_not_found("port").is_recoverable());
721    }
722
723    #[test]
724    fn test_error_codes() {
725        assert_eq!(ProcessError::processing("").code(), "ERR_PROCESSING");
726        assert_eq!(ProcessError::node_not_found(0).code(), "ERR_NODE_NOT_FOUND");
727    }
728
729    #[test]
730    fn test_parameter_error_details() {
731        let err = ParameterError::out_of_range(1.5, 0.0, 1.0);
732        match err {
733            ParameterError::OutOfRange { value, min, max } => {
734                assert_eq!(value, 1.5);
735                assert_eq!(min, 0.0);
736                assert_eq!(max, 1.0);
737            }
738            _ => panic!("Wrong error type"),
739        }
740    }
741}