spring/
error.rs

1use std::io::{self, ErrorKind};
2use thiserror::Error;
3
4/// Spring custom error type
5#[derive(Error, Debug)]
6pub enum AppError {
7    /// component not exists
8    #[error("{0} component not exists")]
9    ComponentNotExist(&'static str),
10
11    /// `.env` file reading failed
12    #[error(transparent)]
13    EnvError(#[from] dotenvy::Error),
14
15    /// File IO Error
16    #[error(transparent)]
17    IOError(#[from] io::Error),
18
19    /// toml file parsing error
20    #[error(transparent)]
21    TomlParseError(#[from] toml::de::Error),
22
23    /// Configuration merge error in toml file
24    #[error("merge toml error: {0}")]
25    TomlMergeError(String),
26
27    /// tokio asynchronous task join failed
28    #[error(transparent)]
29    JoinError(#[from] tokio::task::JoinError),
30
31    /// Deserialization of configuration in toml file to rust struct failed
32    #[error("Failed to deserialize the configuration of prefix \"{0}\": {1}")]
33    DeserializeErr(&'static str, toml::de::Error),
34
35    /// Other runtime errors
36    #[error(transparent)]
37    OtherError(#[from] anyhow::Error),
38}
39
40impl AppError {
41    /// Failed to read file io
42    pub fn from_io(kind: ErrorKind, msg: &str) -> Self {
43        AppError::IOError(io::Error::new(kind, msg))
44    }
45}
46
47/// Contains the return value of AppError
48pub type Result<T> = std::result::Result<T, AppError>;
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn test_component_not_exist_error() {
56        let error = AppError::ComponentNotExist("TestComponent");
57        let error_msg = error.to_string();
58        assert!(error_msg.contains("TestComponent"));
59        assert!(error_msg.contains("component not exists"));
60    }
61
62    #[test]
63    fn test_from_io_error() {
64        let error = AppError::from_io(ErrorKind::NotFound, "file not found");
65        match error {
66            AppError::IOError(e) => {
67                assert_eq!(e.kind(), ErrorKind::NotFound);
68                assert!(e.to_string().contains("file not found"));
69            }
70            _ => panic!("Expected IOError"),
71        }
72    }
73
74    #[test]
75    fn test_toml_merge_error() {
76        let error = AppError::TomlMergeError("conflicting keys".to_string());
77        let error_msg = error.to_string();
78        assert!(error_msg.contains("merge toml error"));
79        assert!(error_msg.contains("conflicting keys"));
80    }
81
82    #[test]
83    fn test_deserialize_error() {
84        let toml_err = toml::from_str::<i32>("invalid").unwrap_err();
85        let error = AppError::DeserializeErr("my-config", toml_err);
86        let error_msg = error.to_string();
87        assert!(error_msg.contains("Failed to deserialize"));
88        assert!(error_msg.contains("my-config"));
89    }
90
91    #[test]
92    fn test_io_error_conversion() {
93        let io_error = io::Error::new(ErrorKind::PermissionDenied, "access denied");
94        let app_error: AppError = io_error.into();
95        
96        match app_error {
97            AppError::IOError(e) => {
98                assert_eq!(e.kind(), ErrorKind::PermissionDenied);
99            }
100            _ => panic!("Expected IOError"),
101        }
102    }
103
104    #[test]
105    fn test_anyhow_error_conversion() {
106        let anyhow_err = anyhow::anyhow!("something went wrong");
107        let app_error: AppError = anyhow_err.into();
108        
109        match app_error {
110            AppError::OtherError(e) => {
111                assert!(e.to_string().contains("something went wrong"));
112            }
113            _ => panic!("Expected OtherError"),
114        }
115    }
116
117    #[test]
118    fn test_error_result_type() {
119        fn returns_error() -> Result<i32> {
120            Err(AppError::ComponentNotExist("TestComponent"))
121        }
122
123        let result = returns_error();
124        assert!(result.is_err());
125        
126        match result {
127            Err(AppError::ComponentNotExist(name)) => {
128                assert_eq!(name, "TestComponent");
129            }
130            _ => panic!("Expected ComponentNotExist error"),
131        }
132    }
133
134    #[test]
135    fn test_error_result_ok() {
136        fn returns_ok() -> Result<String> {
137            Ok("success".to_string())
138        }
139
140        let result = returns_ok();
141        assert!(result.is_ok());
142        assert_eq!(result.unwrap(), "success");
143    }
144
145    #[test]
146    fn test_error_chain() {
147        fn nested_error() -> Result<()> {
148            std::fs::read_to_string("/nonexistent/file.txt")?;
149            Ok(())
150        }
151
152        let result = nested_error();
153        assert!(result.is_err());
154        
155        match result {
156            Err(AppError::IOError(_)) => {
157                // Expected
158            }
159            _ => panic!("Expected IOError from file operation"),
160        }
161    }
162
163    #[test]
164    fn test_all_error_variants_display() {
165        let errors = vec![
166            AppError::ComponentNotExist("Test"),
167            AppError::from_io(ErrorKind::NotFound, "test"),
168            AppError::TomlMergeError("test merge".to_string()),
169        ];
170
171        for error in errors {
172            // All errors should have meaningful display messages
173            let msg = error.to_string();
174            assert!(!msg.is_empty(), "Error message should not be empty");
175        }
176    }
177}