wallswitch/
error.rs

1use crate::{Colors, Dimension, Orientation};
2use std::{
3    io,
4    num::{ParseIntError, TryFromIntError},
5    path::PathBuf,
6    string::FromUtf8Error,
7};
8use thiserror::Error;
9
10/**
11Result type to simplify function signatures.
12
13This is a custom result type that uses our custom `WallSwitchError` for the error type.
14
15Functions can return `WallSwitchResult<T>` and then use `?` to automatically propagate errors.
16*/
17pub type WallSwitchResult<T> = Result<T, WallSwitchError>;
18
19/// WallSwitch Error enum
20///
21/// The `WallSwitchError` enum defines the error values
22///
23/// <https://doc.rust-lang.org/rust-by-example/error/multiple_error_types/define_error_type.html>
24#[derive(Error, Debug)]
25pub enum WallSwitchError {
26    /// Error for command-line arguments that have invalid values.
27    #[error(
28        "{e}: '{a}' value '{v}' must be at least '{n}'. \
29        The condition ({v} >= {n}) is false.\n\n\
30        For more information, try '{h}'.",
31        e = "Error".red().bold(),
32        v = value.yellow(),
33        a = arg.yellow(),
34        n = num.green(),
35        h = "--help".green(),
36    )]
37    AtLeastValue {
38        arg: String,
39        value: String,
40        num: u64,
41    },
42
43    /// Error for command-line arguments that have values exceeding a maximum.
44    #[error(
45        "{e}: '{a}' value '{v}' must be at most '{n}'. \
46        The condition ({v} <= {n}) is false.\n\n\
47        For more information, try '{h}'.",
48        e = "Error".red().bold(),
49        v = value.yellow(),
50        a = arg.yellow(),
51        n = num.green(),
52        h = "--help".green(),
53    )]
54    AtMostValue {
55        arg: String,
56        value: String,
57        num: u64,
58    },
59
60    /// Error when an image path should be disregarded.
61    #[error("Disregard the path: '{p}'.", p = .0.display().yellow(),)]
62    DisregardPath(PathBuf),
63
64    /// Error when failing to convert byte output to a UTF-8 string.
65    #[error("Failed to convert command output to UTF-8: {0}")]
66    FromUtf8(#[from] FromUtf8Error),
67
68    /// Standard I/O error wrapper.
69    #[error("IO error: {0}")]
70    Io(#[from] io::Error),
71
72    /// Error when an image's dimensions are invalid.
73    #[error("{0}")]
74    InvalidDimension(#[from] DimensionError),
75
76    /// Error for file paths that have invalid filenames.
77    #[error(
78        "{e}: Invalid file name --> Disregard the path: '{p}'.",
79        e = "Error".red().bold(),
80        p = .0.display().yellow(),
81    )]
82    InvalidFilename(PathBuf),
83
84    /// Error for image file sizes that are outside an allowed range.
85    #[error(
86        "{e}: invalid file size '{s}' bytes. \
87        The condition ({min} <= {s} <= {max}) is false.",
88        e = "Error".red().bold(),
89        min = min_size.green(),
90        max = max_size.green(),
91        s = size.yellow(),
92    )]
93    InvalidSize {
94        min_size: u64,
95        size: u64,
96        max_size: u64,
97    },
98
99    /// Error for command-line arguments that have invalid values.
100    #[error(
101        "{e}: invalid value '{v}' for '{a}'.\n\n\
102        For more information, try '{h}'.",
103        e = "Error".red().bold(),
104        v = value.yellow(),
105        a = arg.green(),
106        h = "--help".green(),
107    )]
108    InvalidValue { arg: String, value: String },
109
110    /// Error for I/O operations with an associated file path.
111    #[error("{e}: Failed to create file {path:?}\n{io_error}", e = "Error".red().bold())]
112    IOError {
113        path: PathBuf,
114        #[source]
115        io_error: io::Error,
116    },
117
118    /// Error when a JSON serialization or deserialization operation fails.
119    #[error("JSON serialization/deserialization error: {0}")]
120    Json(#[from] serde_json::Error),
121
122    /// Error when obtaining the maximum valid value for a parameter.
123    #[error("Unable to obtain maximum value!")]
124    MaxValue,
125
126    /// Error when obtaining the minimum valid value for a parameter.
127    #[error("Unable to obtain minimum value!")]
128    MinValue,
129
130    /// Error for command-line arguments that are missing required values.
131    #[error(
132        "{e}: missing value for '{a}'.\n\n\
133        For more information, try '{h}'.",
134        e = "Error".red().bold(),
135        a = arg.yellow(),
136        h = "--help".green(),
137    )]
138    MissingValue { arg: String },
139
140    /// Error for minimum value being greater than maximum value.
141    #[error(
142        "{e}: min ({min}) must be less than or equal to max ({max})\n\
143        The condition ({min} <= {max}) is false.",
144        e = "Error".red().bold()
145    )]
146    MinMax { min: u64, max: u64 },
147
148    /// Error when no valid images are found in specified directories.
149    #[error(
150        "{e}: no images found in image directories!\n\
151        directories: {paths:#?}",
152        e = "Error".red().bold(),
153    )]
154    NoImages { paths: Vec<PathBuf> },
155
156    /// Error for invalid image orientation (e.g., neither horizontal nor vertical).
157    #[error(
158        "invalid orientation.\n\n\
159        Valid options: [{h}, {v}]",
160        h = Orientation::Horizontal.green(),
161        v = Orientation::Vertical.green(),
162    )]
163    InvalidOrientation,
164
165    /// Error for an insufficient number of image files found.
166    #[error(
167        "{e}: insufficient number of image files!\n\
168        Found only {n} image file(s):\n\
169        {paths:#?}",
170        n = nfiles.yellow(),
171        e = "Error".red().bold(),
172    )]
173    InsufficientImages { paths: Vec<PathBuf>, nfiles: usize },
174
175    /// Error when an insufficient number of valid images are present.
176    #[error("Insufficient number of valid images!")]
177    InsufficientNumber,
178
179    /// Error when a directory path does not exist.
180    #[error("Wallpaper dir {0:?} does not exist.")]
181    Parent(PathBuf),
182
183    /// Error from a generic conversion attempt.
184    #[error("{0}")]
185    TryInto(String),
186
187    /// Error when a binary or resource cannot be found on the system.
188    #[error("Unable to find '{0}'!")]
189    UnableToFind(String),
190
191    /// Error for unexpected command-line arguments.
192    #[error(
193        "{e}: unexpected argument '{a}' found.\n\n\
194        For more information, try '{h}'.",
195        e = "Error".red().bold(),
196        a = arg.yellow(),
197        h = "--help".green(),
198    )]
199    UnexpectedArg { arg: String },
200}
201
202// Implementing the From trait for multiple types that you want to map into WallSwitchError.
203// https://stackoverflow.com/questions/62238827/less-verbose-type-for-map-err-closure-argument
204// let monitor: u8 = value.try_into().map_err(WallSwitchError::from)?;
205
206impl From<TryFromIntError> for WallSwitchError {
207    fn from(err: TryFromIntError) -> Self {
208        Self::TryInto(err.to_string())
209    }
210}
211
212#[derive(Error, Debug)]
213pub enum DimensionError {
214    #[error(
215        "{error}: invalid dimension '{dimension}'.\n\
216        {log_min}{log_max}\
217        Disregard the path: '{path}'\n",
218        error = "Error".red().bold(),
219        dimension = .dimension.yellow(),
220        log_min = .log_min,
221        log_max = .log_max,
222        path = .path.display().yellow(),
223    )]
224    DimensionFormatError {
225        dimension: Dimension,
226        log_min: String,
227        log_max: String,
228        path: PathBuf,
229    },
230
231    #[error("Invalid dimension format '{0}': failed to parse integer - {1}")]
232    InvalidParse(String, #[source] ParseIntError),
233
234    #[error("Invalid dimension format: expected two numbers (width x height)")]
235    InvalidFormat,
236
237    #[error("Zero is not a valid dimension component")]
238    ZeroDimension,
239}
240
241#[cfg(test)]
242mod error_tests {
243    use crate::{Colors, WallSwitchError};
244    use std::path::PathBuf;
245
246    #[test]
247    /// `cargo test -- --show-output test_error_display`
248    fn test_error_display() {
249        let path = PathBuf::from("/tmp");
250        let text = format!("Disregard the path: '{}'.", path.display().yellow());
251        println!("text: {text}");
252
253        assert_eq!(
254            WallSwitchError::InsufficientNumber.to_string(),
255            "Insufficient number of valid images!"
256        );
257
258        assert_eq!(WallSwitchError::DisregardPath(path).to_string(), text);
259    }
260}