Skip to main content

rill_core/
error.rs

1//! # Rill Core error system
2//!
3//! Centralised error handling for the entire Rill ecosystem.
4//! Provides a hierarchy of error types with context and cross-level
5//! conversion.
6
7use std::error::Error as StdError;
8use std::fmt;
9
10// =============================================================================
11// Main error types
12// =============================================================================
13
14/// Primary error type for the entire Rill ecosystem.
15#[derive(Debug, Clone)]
16pub struct Error {
17    /// High-level error category for grouping.
18    pub category: ErrorCategory,
19    /// Machine-processable error code.
20    pub code: ErrorCode,
21    /// Human-readable error description.
22    pub message: String,
23    /// Optional chained cause (builder-style via [`Error::with_cause`]).
24    pub cause: Option<Box<Error>>,
25    /// Optional source location (attached via [`Error::at`]).
26    pub location: Option<ErrorLocation>,
27}
28
29/// Error category for grouping related error codes.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ErrorCategory {
32    /// Core errors (buffers, queues, basic types).
33    Core,
34    /// DSP errors (filters, effects, generators).
35    Dsp,
36    /// I/O errors (ALSA, JACK, PipeWire).
37    Io,
38    /// Control errors (OSC, automation).
39    Control,
40    /// Configuration errors.
41    Config,
42    /// Runtime errors.
43    Runtime,
44    /// Internal errors (should never occur).
45    Internal,
46}
47
48impl ErrorCategory {
49    /// Return the string representation of this category.
50    pub fn as_str(&self) -> &'static str {
51        match self {
52            ErrorCategory::Core => "core",
53            ErrorCategory::Dsp => "dsp",
54            ErrorCategory::Io => "io",
55            ErrorCategory::Control => "control",
56            ErrorCategory::Config => "config",
57            ErrorCategory::Runtime => "runtime",
58            ErrorCategory::Internal => "internal",
59        }
60    }
61}
62
63impl fmt::Display for ErrorCategory {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "{}", self.as_str())
66    }
67}
68
69/// Machine-processable error code.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum ErrorCode {
72    // ── Core errors (0-99) ──────────────────────────────────────
73    /// Unknown or uncategorised error.
74    Unknown = 0,
75    /// Invalid parameter value.
76    InvalidParameter = 1,
77    /// Operation attempted in an invalid state.
78    InvalidState = 2,
79    /// Unsupported operation.
80    Unsupported = 3,
81    /// Feature not yet implemented.
82    NotImplemented = 4,
83    /// Operation timed out.
84    Timeout = 5,
85
86    // ── Buffer errors (100-119) ─────────────────────────────────
87    /// Buffer is full and cannot accept more data.
88    BufferFull = 100,
89    /// Buffer is empty and has no data to read.
90    BufferEmpty = 101,
91    /// Requested buffer size is invalid.
92    InvalidBufferSize = 102,
93    /// Buffer is misaligned for SIMD operations.
94    BufferMisaligned = 103,
95    /// Buffer has not been initialised yet.
96    BufferNotInitialized = 104,
97
98    // ── Queue errors (120-139) ──────────────────────────────────
99    /// Command or telemetry queue is full.
100    QueueFull = 120,
101    /// Queue is empty (no pending items).
102    QueueEmpty = 121,
103    /// Queue has been closed.
104    QueueClosed = 122,
105    /// Queue index is out of bounds.
106    InvalidQueueIndex = 123,
107
108    // ── I/O errors (300-399) ────────────────────────────────────
109    /// I/O device not found.
110    DeviceNotFound = 300,
111    /// I/O device is busy.
112    DeviceBusy = 301,
113    /// ALSA-specific error.
114    AlsaError = 310,
115    /// JACK-specific error.
116    JackError = 311,
117    /// PipeWire-specific error.
118    PipeWireError = 312,
119    /// Buffer underrun or overrun.
120    XRun = 320,
121
122    // ── Control errors (400-499) ────────────────────────────────
123    /// OSC protocol error.
124    OscError = 401,
125    /// Control mapping not found.
126    MappingNotFound = 402,
127    /// Automaton instance not found.
128    AutomatonNotFound = 403,
129    /// Parameter value is outside the allowed range.
130    InvalidParameterValue = 404,
131
132    // ── Config errors (500-599) ─────────────────────────────────
133    /// Configuration path not found.
134    ConfigNotFound = 500,
135    /// Configuration format is invalid.
136    InvalidConfigFormat = 501,
137    /// Required field is missing from configuration.
138    MissingField = 502,
139
140    // ── Runtime errors (600-699) ────────────────────────────────
141    /// Real-time safety violation detected.
142    RealtimeViolation = 600,
143    /// Failed to set thread priority for RT scheduling.
144    PriorityError = 601,
145    /// Operation failed because the component is already running.
146    AlreadyRunning = 602,
147    /// Operation failed because the component is not running.
148    NotRunning = 603,
149}
150
151impl ErrorCode {
152    /// Return the error category for this code.
153    pub fn category(&self) -> ErrorCategory {
154        match *self {
155            ErrorCode::Unknown
156            | ErrorCode::InvalidParameter
157            | ErrorCode::InvalidState
158            | ErrorCode::Unsupported
159            | ErrorCode::NotImplemented
160            | ErrorCode::Timeout
161            | ErrorCode::BufferFull
162            | ErrorCode::BufferEmpty
163            | ErrorCode::InvalidBufferSize
164            | ErrorCode::BufferMisaligned
165            | ErrorCode::BufferNotInitialized
166            | ErrorCode::QueueFull
167            | ErrorCode::QueueEmpty
168            | ErrorCode::QueueClosed
169            | ErrorCode::InvalidQueueIndex => ErrorCategory::Core,
170
171            ErrorCode::DeviceNotFound
172            | ErrorCode::DeviceBusy
173            | ErrorCode::AlsaError
174            | ErrorCode::JackError
175            | ErrorCode::PipeWireError
176            | ErrorCode::XRun => ErrorCategory::Io,
177
178            ErrorCode::OscError
179            | ErrorCode::MappingNotFound
180            | ErrorCode::AutomatonNotFound
181            | ErrorCode::InvalidParameterValue => ErrorCategory::Control,
182
183            ErrorCode::ConfigNotFound
184            | ErrorCode::InvalidConfigFormat
185            | ErrorCode::MissingField => ErrorCategory::Config,
186
187            ErrorCode::RealtimeViolation
188            | ErrorCode::PriorityError
189            | ErrorCode::AlreadyRunning
190            | ErrorCode::NotRunning => ErrorCategory::Runtime,
191        }
192    }
193
194    /// Return a human-readable description of this error code.
195    pub fn description(&self) -> &'static str {
196        match self {
197            ErrorCode::Unknown => "Unknown error",
198            ErrorCode::InvalidParameter => "Invalid parameter",
199            ErrorCode::InvalidState => "Invalid state",
200            ErrorCode::Unsupported => "Unsupported operation",
201            ErrorCode::NotImplemented => "Not implemented",
202            ErrorCode::Timeout => "Operation timed out",
203
204            ErrorCode::BufferFull => "Buffer is full",
205            ErrorCode::BufferEmpty => "Buffer is empty",
206            ErrorCode::InvalidBufferSize => "Invalid buffer size",
207            ErrorCode::BufferMisaligned => "Buffer is misaligned for SIMD operations",
208            ErrorCode::BufferNotInitialized => "Buffer not initialized",
209
210            ErrorCode::QueueFull => "Queue is full",
211            ErrorCode::QueueEmpty => "Queue is empty",
212            ErrorCode::QueueClosed => "Queue is closed",
213            ErrorCode::InvalidQueueIndex => "Invalid queue index",
214
215            ErrorCode::DeviceNotFound => "Device not found",
216            ErrorCode::DeviceBusy => "Device is busy",
217            ErrorCode::AlsaError => "ALSA error",
218            ErrorCode::JackError => "JACK error",
219            ErrorCode::PipeWireError => "PipeWire error",
220            ErrorCode::XRun => "Buffer underrun/overrun detected",
221
222            ErrorCode::OscError => "OSC error",
223            ErrorCode::MappingNotFound => "Mapping not found",
224            ErrorCode::AutomatonNotFound => "Automaton not found",
225            ErrorCode::InvalidParameterValue => "Invalid parameter value",
226
227            ErrorCode::ConfigNotFound => "Configuration not found",
228            ErrorCode::InvalidConfigFormat => "Invalid configuration format",
229            ErrorCode::MissingField => "Missing required field",
230
231            ErrorCode::RealtimeViolation => "Real-time violation detected",
232            ErrorCode::PriorityError => "Failed to set thread priority",
233            ErrorCode::AlreadyRunning => "Already running",
234            ErrorCode::NotRunning => "Not running",
235        }
236    }
237}
238
239/// Source location where an error originated.
240#[derive(Debug, Clone)]
241pub struct ErrorLocation {
242    /// Source file name.
243    pub file: &'static str,
244    /// Line number in the source file.
245    pub line: u32,
246    /// Column number in the source file.
247    pub column: u32,
248}
249
250impl fmt::Display for ErrorLocation {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        write!(f, "{}:{}:{}", self.file, self.line, self.column)
253    }
254}
255
256// =============================================================================
257// Error implementation
258// =============================================================================
259
260impl Error {
261    /// Create a new error with the given code and message.
262    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
263        Self {
264            category: code.category(),
265            code,
266            message: message.into(),
267            cause: None,
268            location: None,
269        }
270    }
271
272    /// Add a cause to this error (builder-style).
273    pub fn with_cause(mut self, cause: Error) -> Self {
274        self.cause = Some(Box::new(cause));
275        self
276    }
277
278    /// Attach source location info (builder-style).
279    pub fn at(mut self, file: &'static str, line: u32, column: u32) -> Self {
280        self.location = Some(ErrorLocation { file, line, column });
281        self
282    }
283
284    /// Walk the cause chain to find the root cause.
285    pub fn root_cause(&self) -> &Error {
286        let mut current = self;
287        while let Some(cause) = &current.cause {
288            current = cause;
289        }
290        current
291    }
292
293    /// Whether this error is critical for a real-time thread.
294    pub fn is_realtime_critical(&self) -> bool {
295        matches!(
296            self.code,
297            ErrorCode::RealtimeViolation
298                | ErrorCode::PriorityError
299                | ErrorCode::BufferFull
300                | ErrorCode::XRun
301        )
302    }
303
304    /// Whether this error is recoverable (non-critical).
305    pub fn is_recoverable(&self) -> bool {
306        !self.is_realtime_critical()
307    }
308}
309
310impl fmt::Display for Error {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        if let Some(loc) = &self.location {
313            write!(
314                f,
315                "[{}] at {}: {} ({})",
316                self.category,
317                loc,
318                self.message,
319                self.code.description()
320            )?;
321        } else {
322            write!(
323                f,
324                "[{}]: {} ({})",
325                self.category,
326                self.message,
327                self.code.description()
328            )?;
329        }
330
331        if let Some(cause) = &self.cause {
332            write!(f, "\n  caused by: {}", cause)?;
333        }
334
335        Ok(())
336    }
337}
338
339impl StdError for Error {
340    fn source(&self) -> Option<&(dyn StdError + 'static)> {
341        self.cause.as_ref().map(|c| c as &dyn StdError)
342    }
343}
344
345// =============================================================================
346// Result type
347// =============================================================================
348
349/// Result type alias for Rill Core operations.
350pub type Result<T> = std::result::Result<T, Error>;
351
352// =============================================================================
353// Conversion from standard errors
354// =============================================================================
355
356impl From<std::io::Error> for Error {
357    fn from(err: std::io::Error) -> Self {
358        Error::new(ErrorCode::Unknown, err.to_string())
359    }
360}
361
362impl From<std::num::ParseIntError> for Error {
363    fn from(err: std::num::ParseIntError) -> Self {
364        Error::new(ErrorCode::InvalidParameter, err.to_string())
365    }
366}
367
368impl From<std::num::ParseFloatError> for Error {
369    fn from(err: std::num::ParseFloatError) -> Self {
370        Error::new(ErrorCode::InvalidParameter, err.to_string())
371    }
372}
373
374impl From<std::str::Utf8Error> for Error {
375    fn from(err: std::str::Utf8Error) -> Self {
376        Error::new(ErrorCode::InvalidParameter, err.to_string())
377    }
378}
379
380// =============================================================================
381// Macros for convenient error creation
382// =============================================================================
383
384/// Create an error with a code and message.
385#[macro_export]
386macro_rules! error {
387    ($code:expr, $msg:expr) => {
388        $crate::error::Error::new($code, $msg)
389    };
390    ($code:expr, $fmt:expr, $($arg:tt)*) => {
391        $crate::error::Error::new($code, format!($fmt, $($arg)*))
392    };
393}
394
395// =============================================================================
396// Specialized error types for different components
397// =============================================================================
398
399/// I/O error constructors.
400pub mod io {
401    #![allow(unused)]
402    use super::*;
403
404    /// Create a `DeviceNotFound` error.
405    pub fn device_not_found(name: &str) -> Error {
406        error!(ErrorCode::DeviceNotFound, "Device not found: {}", name)
407    }
408
409    /// Create a `DeviceBusy` error.
410    pub fn device_busy(name: &str) -> Error {
411        error!(ErrorCode::DeviceBusy, "Device is busy: {}", name)
412    }
413
414    /// Create an `AlsaError` with a description.
415    pub fn alsa_error(desc: &str) -> Error {
416        error!(ErrorCode::AlsaError, "ALSA error: {}", desc)
417    }
418
419    /// Create a `JackError` with a description.
420    pub fn jack_error(desc: &str) -> Error {
421        error!(ErrorCode::JackError, "JACK error: {}", desc)
422    }
423
424    /// Create a `PipeWireError` with a description.
425    pub fn pipewire_error(desc: &str) -> Error {
426        error!(ErrorCode::PipeWireError, "PipeWire error: {}", desc)
427    }
428
429    /// Create an `XRun` (buffer underrun/overrun) error.
430    pub fn xrun() -> Error {
431        Error::new(ErrorCode::XRun, "Buffer underrun/overrun detected")
432    }
433}
434
435/// Control error constructors (OSC, automation).
436pub mod control {
437    use super::*;
438
439    /// Create an `OscError` with a description.
440    pub fn osc_error(desc: &str) -> Error {
441        error!(ErrorCode::OscError, "OSC error: {}", desc)
442    }
443
444    /// Create a `MappingNotFound` error for the given mapping ID.
445    pub fn mapping_not_found(id: &str) -> Error {
446        error!(ErrorCode::MappingNotFound, "Mapping not found: {}", id)
447    }
448
449    /// Create an `AutomatonNotFound` error for the given automaton ID.
450    pub fn automaton_not_found(id: &str) -> Error {
451        error!(ErrorCode::AutomatonNotFound, "Automaton not found: {}", id)
452    }
453
454    /// Create an `InvalidParameterValue` error for a value outside the allowed range.
455    pub fn invalid_parameter_value(param: &str, value: f64, min: f64, max: f64) -> Error {
456        error!(
457            ErrorCode::InvalidParameterValue,
458            "Invalid value for parameter {}: {} (allowed range: {} - {})", param, value, min, max
459        )
460    }
461}
462
463/// Configuration error constructors.
464pub mod config {
465    use super::*;
466
467    /// Create a `ConfigNotFound` error for the given path.
468    pub fn not_found(path: &str) -> Error {
469        error!(
470            ErrorCode::ConfigNotFound,
471            "Configuration not found: {}", path
472        )
473    }
474
475    /// Create an `InvalidConfigFormat` error with details.
476    pub fn invalid_format(details: &str) -> Error {
477        error!(
478            ErrorCode::InvalidConfigFormat,
479            "Invalid configuration format: {}", details
480        )
481    }
482
483    /// Create a `MissingField` error for the required field name.
484    pub fn missing_field(field: &str) -> Error {
485        error!(ErrorCode::MissingField, "Missing required field: {}", field)
486    }
487}
488
489/// Runtime error constructors (thread priority, critical violations).
490pub mod runtime {
491    use super::*;
492
493    /// Create a `RealtimeViolation` error with details.
494    pub fn realtime_violation(details: &str) -> Error {
495        error!(
496            ErrorCode::RealtimeViolation,
497            "Real-time violation: {}", details
498        )
499    }
500
501    /// Create a `PriorityError` with details about the failure.
502    pub fn priority_error(details: &str) -> Error {
503        error!(
504            ErrorCode::PriorityError,
505            "Failed to set thread priority: {}", details
506        )
507    }
508
509    /// Create an `AlreadyRunning` error.
510    pub fn already_running() -> Error {
511        Error::new(ErrorCode::AlreadyRunning, "Already running")
512    }
513
514    /// Create a `NotRunning` error.
515    pub fn not_running() -> Error {
516        Error::new(ErrorCode::NotRunning, "Not running")
517    }
518}
519
520// =============================================================================
521// Tests
522// =============================================================================
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    #[test]
529    fn test_error_creation() {
530        let err = Error::new(ErrorCode::BufferFull, "Test error");
531        assert_eq!(err.code, ErrorCode::BufferFull);
532        assert_eq!(err.message, "Test error");
533        assert_eq!(err.category, ErrorCategory::Core);
534    }
535
536    #[test]
537    fn test_error_with_cause() {
538        let cause = Error::new(ErrorCode::BufferEmpty, "Cause");
539        let err = Error::new(ErrorCode::BufferFull, "Main error").with_cause(cause);
540
541        assert!(err.cause.is_some());
542        assert_eq!(err.root_cause().code, ErrorCode::BufferEmpty);
543    }
544
545    #[test]
546    fn test_error_macros() {
547        let err = error!(ErrorCode::BufferFull, "Buffer is full");
548        assert_eq!(err.code, ErrorCode::BufferFull);
549
550        let err = error!(ErrorCode::BufferFull, "Buffer {} is full", "test");
551        assert_eq!(err.message, "Buffer test is full");
552    }
553
554    #[test]
555    fn test_specialized_errors() {
556        let err = io::device_not_found("hw:0");
557        assert_eq!(err.code, ErrorCode::DeviceNotFound);
558        assert!(err.message.contains("hw:0"));
559    }
560
561    #[test]
562    fn test_error_category() {
563        assert_eq!(ErrorCode::BufferFull.category(), ErrorCategory::Core);
564        assert_eq!(ErrorCode::AlsaError.category(), ErrorCategory::Io);
565        assert_eq!(ErrorCode::OscError.category(), ErrorCategory::Control);
566        assert_eq!(ErrorCode::ConfigNotFound.category(), ErrorCategory::Config);
567        assert_eq!(
568            ErrorCode::RealtimeViolation.category(),
569            ErrorCategory::Runtime
570        );
571    }
572
573    #[test]
574    fn test_realtime_critical() {
575        assert!(io::xrun().is_realtime_critical());
576        assert!(runtime::realtime_violation("test").is_realtime_critical());
577
578        assert!(!config::not_found("test").is_realtime_critical());
579    }
580}