Skip to main content

monsoon_cli/cli/
error.rs

1//! Error types for the CLI module.
2//!
3//! This module provides a comprehensive error type system for the CLI,
4//! enabling precise error handling and helpful error messages.
5//!
6//! # Design Goals
7//!
8//! - **Structured errors**: Each error type captures context for debugging
9//! - **Helpful messages**: Error display includes suggestions when possible
10//! - **Easy matching**: Errors can be matched on to handle specific cases
11//! - **Composable**: Errors can wrap underlying causes
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use lockstep::cli::error::{CliError, CliResult};
17//!
18//! fn load_config(path: &str) -> CliResult<Config> {
19//!     let content = std::fs::read_to_string(path)
20//!         .map_err(|e| CliError::config_io(path, e))?;
21//!     
22//!     toml::from_str(&content)
23//!         .map_err(|e| CliError::config_parse(path, e))
24//! }
25//! ```
26
27use std::fmt;
28use std::path::PathBuf;
29
30/// Result type alias for CLI operations.
31pub type CliResult<T> = Result<T, CliError>;
32
33/// Comprehensive error type for CLI operations.
34///
35/// This enum covers all possible error conditions in the CLI module,
36/// providing structured information for error handling and reporting.
37#[derive(Debug, Clone)]
38pub enum CliError {
39    // =========================================================================
40    // Argument Errors
41    // =========================================================================
42    /// Invalid argument value
43    InvalidArgument {
44        arg: String,
45        value: String,
46        reason: String,
47        hint: Option<String>,
48    },
49
50    /// Missing required argument
51    MissingArgument { arg: String, context: String },
52
53    /// Conflicting arguments specified
54    ConflictingArguments {
55        arg1: String,
56        arg2: String,
57        reason: String,
58    },
59
60    /// Invalid combination of arguments
61    InvalidArgumentCombination { args: Vec<String>, reason: String },
62
63    // =========================================================================
64    // Config File Errors
65    // =========================================================================
66    /// Failed to read config file
67    ConfigIo { path: PathBuf, message: String },
68
69    /// Failed to parse config file
70    ConfigParse {
71        path: PathBuf,
72        message: String,
73        line: Option<usize>,
74    },
75
76    /// Invalid config value
77    ConfigValue {
78        path: PathBuf,
79        key: String,
80        value: String,
81        reason: String,
82    },
83
84    // =========================================================================
85    // ROM Errors
86    // =========================================================================
87    /// Failed to load ROM
88    RomLoad { path: PathBuf, message: String },
89
90    /// ROM not found
91    RomNotFound { path: PathBuf },
92
93    /// Invalid ROM format
94    RomInvalid { path: PathBuf, reason: String },
95
96    // =========================================================================
97    // Savestate Errors
98    // =========================================================================
99    /// Failed to load savestate
100    SavestateLoad { source: String, message: String },
101
102    /// Failed to save savestate
103    SavestateSave {
104        destination: String,
105        message: String,
106    },
107
108    /// Invalid savestate format
109    SavestateInvalid { source: String, reason: String },
110
111    // =========================================================================
112    // Memory Errors
113    // =========================================================================
114    /// Invalid memory address
115    InvalidAddress { address: String, reason: String },
116
117    /// Invalid memory range
118    InvalidMemoryRange {
119        range: String,
120        reason: String,
121        hint: Option<String>,
122    },
123
124    /// Memory access error
125    MemoryAccess {
126        operation: String,
127        address: u16,
128        message: String,
129    },
130
131    // =========================================================================
132    // Execution Errors
133    // =========================================================================
134    /// Execution error
135    Execution {
136        message: String,
137        cycles: Option<u128>,
138        frames: Option<u64>,
139    },
140
141    /// Invalid stop condition
142    InvalidStopCondition {
143        condition: String,
144        reason: String,
145        hint: Option<String>,
146    },
147
148    // =========================================================================
149    // Output Errors
150    // =========================================================================
151    /// Failed to write output
152    OutputWrite {
153        destination: String,
154        message: String,
155    },
156
157    /// Invalid output format
158    InvalidOutputFormat {
159        format: String,
160        valid_formats: Vec<String>,
161    },
162
163    // =========================================================================
164    // Generic Errors
165    // =========================================================================
166    /// I/O error
167    Io { operation: String, message: String },
168
169    /// Internal error (should not happen)
170    Internal { message: String },
171}
172
173impl CliError {
174    // =========================================================================
175    // Argument Error Constructors
176    // =========================================================================
177
178    /// Create an invalid argument error.
179    pub fn invalid_arg(
180        arg: impl Into<String>,
181        value: impl Into<String>,
182        reason: impl Into<String>,
183    ) -> Self {
184        Self::InvalidArgument {
185            arg: arg.into(),
186            value: value.into(),
187            reason: reason.into(),
188            hint: None,
189        }
190    }
191
192    /// Create an invalid argument error with a hint.
193    pub fn invalid_arg_with_hint(
194        arg: impl Into<String>,
195        value: impl Into<String>,
196        reason: impl Into<String>,
197        hint: impl Into<String>,
198    ) -> Self {
199        Self::InvalidArgument {
200            arg: arg.into(),
201            value: value.into(),
202            reason: reason.into(),
203            hint: Some(hint.into()),
204        }
205    }
206
207    /// Create a missing argument error.
208    pub fn missing_arg(arg: impl Into<String>, context: impl Into<String>) -> Self {
209        Self::MissingArgument {
210            arg: arg.into(),
211            context: context.into(),
212        }
213    }
214
215    /// Create a conflicting arguments error.
216    pub fn conflicting_args(
217        arg1: impl Into<String>,
218        arg2: impl Into<String>,
219        reason: impl Into<String>,
220    ) -> Self {
221        Self::ConflictingArguments {
222            arg1: arg1.into(),
223            arg2: arg2.into(),
224            reason: reason.into(),
225        }
226    }
227
228    // =========================================================================
229    // Config Error Constructors
230    // =========================================================================
231
232    /// Create a config I/O error.
233    pub fn config_io(path: impl Into<PathBuf>, err: impl fmt::Display) -> Self {
234        Self::ConfigIo {
235            path: path.into(),
236            message: err.to_string(),
237        }
238    }
239
240    /// Create a config parse error.
241    pub fn config_parse(path: impl Into<PathBuf>, err: impl fmt::Display) -> Self {
242        Self::ConfigParse {
243            path: path.into(),
244            message: err.to_string(),
245            line: None,
246        }
247    }
248
249    // =========================================================================
250    // Memory Error Constructors
251    // =========================================================================
252
253    /// Create an invalid memory range error.
254    pub fn invalid_memory_range(range: impl Into<String>, reason: impl Into<String>) -> Self {
255        Self::InvalidMemoryRange {
256            range: range.into(),
257            reason: reason.into(),
258            hint: Some(
259                "Use START-END (e.g., 0x0000-0x07FF) or START:LENGTH (e.g., 0x6000:0x100)".into(),
260            ),
261        }
262    }
263
264    /// Create an invalid address error.
265    pub fn invalid_address(address: impl Into<String>, reason: impl Into<String>) -> Self {
266        Self::InvalidAddress {
267            address: address.into(),
268            reason: reason.into(),
269        }
270    }
271
272    // =========================================================================
273    // Savestate Error Constructors
274    // =========================================================================
275
276    /// Create a savestate load error.
277    pub fn savestate_load(source: impl Into<String>, err: impl fmt::Display) -> Self {
278        Self::SavestateLoad {
279            source: source.into(),
280            message: err.to_string(),
281        }
282    }
283
284    /// Create a savestate save error.
285    pub fn savestate_save(destination: impl Into<String>, err: impl fmt::Display) -> Self {
286        Self::SavestateSave {
287            destination: destination.into(),
288            message: err.to_string(),
289        }
290    }
291
292    // =========================================================================
293    // Execution Error Constructors
294    // =========================================================================
295
296    /// Create an execution error.
297    pub fn execution(message: impl Into<String>) -> Self {
298        Self::Execution {
299            message: message.into(),
300            cycles: None,
301            frames: None,
302        }
303    }
304
305    /// Create an invalid stop condition error.
306    pub fn invalid_stop_condition(condition: impl Into<String>, reason: impl Into<String>) -> Self {
307        Self::InvalidStopCondition {
308            condition: condition.into(),
309            reason: reason.into(),
310            hint: Some("Valid formats: ADDR==VALUE, ADDR!=VALUE (e.g., 0x6000==0x80)".into()),
311        }
312    }
313
314    // =========================================================================
315    // Output Error Constructors
316    // =========================================================================
317
318    /// Create an output write error.
319    pub fn output_write(destination: impl Into<String>, err: impl fmt::Display) -> Self {
320        Self::OutputWrite {
321            destination: destination.into(),
322            message: err.to_string(),
323        }
324    }
325
326    // =========================================================================
327    // Generic Error Constructors
328    // =========================================================================
329
330    /// Create an I/O error.
331    pub fn io(operation: impl Into<String>, err: impl fmt::Display) -> Self {
332        Self::Io {
333            operation: operation.into(),
334            message: err.to_string(),
335        }
336    }
337
338    /// Create an internal error.
339    pub fn internal(message: impl Into<String>) -> Self {
340        Self::Internal {
341            message: message.into(),
342        }
343    }
344
345    // =========================================================================
346    // Error Classification
347    // =========================================================================
348
349    /// Check if this is a user error (invalid input).
350    pub fn is_user_error(&self) -> bool {
351        matches!(
352            self,
353            Self::InvalidArgument { .. }
354                | Self::MissingArgument { .. }
355                | Self::ConflictingArguments { .. }
356                | Self::InvalidArgumentCombination { .. }
357                | Self::ConfigParse { .. }
358                | Self::ConfigValue { .. }
359                | Self::InvalidAddress { .. }
360                | Self::InvalidMemoryRange { .. }
361                | Self::InvalidStopCondition { .. }
362                | Self::InvalidOutputFormat { .. }
363        )
364    }
365
366    /// Check if this is an I/O error.
367    pub fn is_io_error(&self) -> bool {
368        matches!(
369            self,
370            Self::ConfigIo { .. }
371                | Self::RomLoad { .. }
372                | Self::RomNotFound { .. }
373                | Self::SavestateLoad { .. }
374                | Self::SavestateSave { .. }
375                | Self::OutputWrite { .. }
376                | Self::Io { .. }
377        )
378    }
379
380    /// Get the exit code for this error.
381    pub fn exit_code(&self) -> u8 {
382        match self {
383            // Invalid arguments
384            Self::InvalidArgument {
385                ..
386            }
387            | Self::MissingArgument {
388                ..
389            }
390            | Self::ConflictingArguments {
391                ..
392            }
393            | Self::InvalidArgumentCombination {
394                ..
395            } => 2,
396
397            // ROM errors
398            Self::RomLoad {
399                ..
400            }
401            | Self::RomNotFound {
402                ..
403            }
404            | Self::RomInvalid {
405                ..
406            } => 3,
407
408            // Savestate errors
409            Self::SavestateLoad {
410                ..
411            }
412            | Self::SavestateSave {
413                ..
414            }
415            | Self::SavestateInvalid {
416                ..
417            } => 4,
418
419            // I/O errors
420            Self::ConfigIo {
421                ..
422            }
423            | Self::OutputWrite {
424                ..
425            }
426            | Self::Io {
427                ..
428            } => 5,
429
430            // Other errors
431            _ => 1,
432        }
433    }
434}
435
436impl fmt::Display for CliError {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        match self {
439            // Argument errors
440            Self::InvalidArgument {
441                arg,
442                value,
443                reason,
444                hint,
445            } => {
446                write!(
447                    f,
448                    "Invalid value '{}' for argument '{}': {}",
449                    value, arg, reason
450                )?;
451                if let Some(h) = hint {
452                    write!(f, "\nHint: {}", h)?;
453                }
454                Ok(())
455            }
456            Self::MissingArgument {
457                arg,
458                context,
459            } => {
460                write!(f, "Missing required argument '{}': {}", arg, context)
461            }
462            Self::ConflictingArguments {
463                arg1,
464                arg2,
465                reason,
466            } => {
467                write!(
468                    f,
469                    "Cannot use '{}' and '{}' together: {}",
470                    arg1, arg2, reason
471                )
472            }
473            Self::InvalidArgumentCombination {
474                args,
475                reason,
476            } => {
477                write!(
478                    f,
479                    "Invalid argument combination [{}]: {}",
480                    args.join(", "),
481                    reason
482                )
483            }
484
485            // Config errors
486            Self::ConfigIo {
487                path,
488                message,
489            } => {
490                write!(
491                    f,
492                    "Failed to read config file '{}': {}",
493                    path.display(),
494                    message
495                )
496            }
497            Self::ConfigParse {
498                path,
499                message,
500                line,
501            } => {
502                write!(f, "Failed to parse config file '{}'", path.display())?;
503                if let Some(l) = line {
504                    write!(f, " at line {}", l)?;
505                }
506                write!(f, ": {}", message)
507            }
508            Self::ConfigValue {
509                path,
510                key,
511                value,
512                reason,
513            } => {
514                write!(
515                    f,
516                    "Invalid value '{}' for key '{}' in config '{}': {}",
517                    value,
518                    key,
519                    path.display(),
520                    reason
521                )
522            }
523
524            // ROM errors
525            Self::RomLoad {
526                path,
527                message,
528            } => {
529                write!(f, "Failed to load ROM '{}': {}", path.display(), message)
530            }
531            Self::RomNotFound {
532                path,
533            } => {
534                write!(f, "ROM file not found: {}", path.display())
535            }
536            Self::RomInvalid {
537                path,
538                reason,
539            } => {
540                write!(f, "Invalid ROM file '{}': {}", path.display(), reason)
541            }
542
543            // Savestate errors
544            Self::SavestateLoad {
545                source,
546                message,
547            } => {
548                write!(f, "Failed to load savestate from {}: {}", source, message)
549            }
550            Self::SavestateSave {
551                destination,
552                message,
553            } => {
554                write!(
555                    f,
556                    "Failed to save savestate to {}: {}",
557                    destination, message
558                )
559            }
560            Self::SavestateInvalid {
561                source,
562                reason,
563            } => {
564                write!(f, "Invalid savestate from {}: {}", source, reason)
565            }
566
567            // Memory errors
568            Self::InvalidAddress {
569                address,
570                reason,
571            } => {
572                write!(f, "Invalid address '{}': {}", address, reason)
573            }
574            Self::InvalidMemoryRange {
575                range,
576                reason,
577                hint,
578            } => {
579                write!(f, "Invalid memory range '{}': {}", range, reason)?;
580                if let Some(h) = hint {
581                    write!(f, "\nHint: {}", h)?;
582                }
583                Ok(())
584            }
585            Self::MemoryAccess {
586                operation,
587                address,
588                message,
589            } => {
590                write!(
591                    f,
592                    "Memory {} at 0x{:04X} failed: {}",
593                    operation, address, message
594                )
595            }
596
597            // Execution errors
598            Self::Execution {
599                message,
600                cycles,
601                frames,
602            } => {
603                write!(f, "Execution error: {}", message)?;
604                if let Some(c) = cycles {
605                    write!(f, " (after {} cycles", c)?;
606                    if let Some(fr) = frames {
607                        write!(f, ", {} frames", fr)?;
608                    }
609                    write!(f, ")")?;
610                }
611                Ok(())
612            }
613            Self::InvalidStopCondition {
614                condition,
615                reason,
616                hint,
617            } => {
618                write!(f, "Invalid stop condition '{}': {}", condition, reason)?;
619                if let Some(h) = hint {
620                    write!(f, "\nHint: {}", h)?;
621                }
622                Ok(())
623            }
624
625            // Output errors
626            Self::OutputWrite {
627                destination,
628                message,
629            } => {
630                write!(f, "Failed to write output to {}: {}", destination, message)
631            }
632            Self::InvalidOutputFormat {
633                format,
634                valid_formats,
635            } => {
636                write!(
637                    f,
638                    "Invalid output format '{}'. Valid formats: {}",
639                    format,
640                    valid_formats.join(", ")
641                )
642            }
643
644            // Generic errors
645            Self::Io {
646                operation,
647                message,
648            } => {
649                write!(f, "I/O error during {}: {}", operation, message)
650            }
651            Self::Internal {
652                message,
653            } => {
654                write!(f, "Internal error: {}", message)
655            }
656        }
657    }
658}
659
660impl std::error::Error for CliError {}
661
662// =========================================================================
663// Conversions
664// =========================================================================
665
666impl From<std::io::Error> for CliError {
667    fn from(err: std::io::Error) -> Self {
668        Self::Io {
669            operation: "I/O operation".into(),
670            message: err.to_string(),
671        }
672    }
673}
674
675impl From<toml::de::Error> for CliError {
676    fn from(err: toml::de::Error) -> Self {
677        Self::ConfigParse {
678            path: PathBuf::new(),
679            message: err.to_string(),
680            // Note: TOML errors don't provide line numbers directly,
681            // the error message itself contains location info
682            line: None,
683        }
684    }
685}
686
687// =========================================================================
688// Tests
689// =========================================================================
690
691#[cfg(test)]
692mod tests {
693    use super::*;
694
695    #[test]
696    fn test_error_display() {
697        let err = CliError::invalid_arg("--frames", "-5", "must be positive");
698        assert!(err.to_string().contains("--frames"));
699        assert!(err.to_string().contains("-5"));
700        assert!(err.to_string().contains("positive"));
701    }
702
703    #[test]
704    fn test_error_with_hint() {
705        let err = CliError::invalid_arg_with_hint(
706            "--until-pc",
707            "invalid",
708            "not a valid hex address",
709            "Use format 0xADDR or ADDR",
710        );
711        let msg = err.to_string();
712        assert!(msg.contains("Hint:"));
713    }
714
715    #[test]
716    fn test_conflicting_args() {
717        let err = CliError::conflicting_args(
718            "--state-stdin",
719            "--load-state",
720            "can only use one input source",
721        );
722        assert!(err.to_string().contains("--state-stdin"));
723        assert!(err.to_string().contains("--load-state"));
724    }
725
726    #[test]
727    fn test_exit_codes() {
728        assert_eq!(CliError::invalid_arg("a", "b", "c").exit_code(), 2);
729        assert_eq!(
730            CliError::RomNotFound {
731                path: PathBuf::new()
732            }
733            .exit_code(),
734            3
735        );
736        assert_eq!(CliError::savestate_load("file", "err").exit_code(), 4);
737        assert_eq!(CliError::io("read", "err").exit_code(), 5);
738    }
739
740    #[test]
741    fn test_error_classification() {
742        let user_err = CliError::invalid_arg("a", "b", "c");
743        let io_err = CliError::io("read", "failed");
744
745        assert!(user_err.is_user_error());
746        assert!(!user_err.is_io_error());
747
748        assert!(!io_err.is_user_error());
749        assert!(io_err.is_io_error());
750    }
751
752    #[test]
753    fn test_memory_range_error() {
754        let err = CliError::invalid_memory_range("invalid", "bad format");
755        let msg = err.to_string();
756        assert!(msg.contains("invalid"));
757        assert!(msg.contains("Hint:"));
758        assert!(msg.contains("START-END"));
759    }
760}