Skip to main content

dampen_cli/commands/add/
errors.rs

1//! Error types for the add command.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Errors that can occur during window name validation.
7#[derive(Debug, Error)]
8pub enum ValidationError {
9    /// Window name is empty
10    #[error("Window name cannot be empty")]
11    EmptyName,
12
13    /// Window name starts with an invalid character
14    #[error("Window name must start with a letter or underscore, found '{0}'")]
15    InvalidFirstChar(char),
16
17    /// Window name contains invalid characters
18    #[error(
19        "Window name contains invalid characters (only letters, numbers, and underscores allowed)"
20    )]
21    InvalidCharacters,
22
23    /// Window name is a reserved keyword
24    #[error("'{0}' is a reserved name")]
25    ReservedName(String),
26}
27
28/// Errors that can occur during path resolution and validation.
29#[derive(Debug, Error)]
30pub enum PathError {
31    /// Path is absolute (not allowed)
32    #[error(
33        "Absolute paths are not allowed: {0}\nhelp: Use a relative path within the project, e.g., 'src/ui/orders/'"
34    )]
35    AbsolutePath(PathBuf),
36
37    /// Path escapes project directory
38    #[error(
39        "Path '{path}' is outside the project directory\nhelp: Use a relative path within the project, e.g., 'src/ui/orders/'"
40    )]
41    OutsideProject {
42        /// The problematic path
43        path: PathBuf,
44        /// The project root for reference
45        project_root: PathBuf,
46    },
47
48    /// I/O error during path operations
49    #[error("I/O error: {0}")]
50    Io(#[from] std::io::Error),
51}
52
53/// Errors that can occur during project detection and validation.
54#[derive(Debug, Error)]
55pub enum ProjectError {
56    /// Cargo.toml not found in current directory or parents
57    #[error(
58        "Not a Dampen project: Cargo.toml not found in current directory or any parent directory\nhelp: Run 'dampen new <project_name>' to create a new Dampen project"
59    )]
60    CargoTomlNotFound,
61
62    /// Project doesn't have dampen-core dependency
63    #[error(
64        "Not a Dampen project: dampen-core dependency not found in Cargo.toml\nhelp: Add dampen-core to [dependencies] or run 'dampen new' to create a new project"
65    )]
66    NotDampenProject,
67
68    /// I/O error reading Cargo.toml
69    #[error("Failed to read Cargo.toml: {0}")]
70    IoError(#[from] std::io::Error),
71
72    /// Error parsing Cargo.toml
73    #[error("Failed to parse Cargo.toml: {0}")]
74    ParseError(#[from] toml::de::Error),
75}
76
77/// Errors that can occur during file generation.
78#[derive(Debug, Error)]
79pub enum GenerationError {
80    /// Window file already exists
81    #[error(
82        "Window '{window_name}' already exists at {path}\nhelp: Choose a different name or remove the existing file first"
83    )]
84    FileExists {
85        /// The window name that conflicts
86        window_name: String,
87        /// The path to the conflicting file
88        path: PathBuf,
89    },
90
91    /// Failed to create directory
92    #[error("Failed to create directory {path}: {source}")]
93    DirectoryCreation {
94        /// The directory path
95        path: PathBuf,
96        /// The underlying I/O error
97        #[source]
98        source: std::io::Error,
99    },
100
101    /// Failed to write file
102    #[error("Failed to write file {path}: {source}")]
103    FileWrite {
104        /// The file path
105        path: PathBuf,
106        /// The underlying I/O error
107        #[source]
108        source: std::io::Error,
109    },
110}
111
112/// Errors that can occur during automatic integration of modules.
113#[derive(Debug, Error)]
114pub enum IntegrationError {
115    /// Failed to read a mod.rs file
116    #[error("Failed to read {path}: {source}")]
117    ModFileRead {
118        /// The mod.rs file path
119        path: PathBuf,
120        /// The underlying I/O error
121        #[source]
122        source: std::io::Error,
123    },
124
125    /// Failed to write a mod.rs file
126    #[error("Failed to write {path}: {source}")]
127    ModFileWrite {
128        /// The mod.rs file path
129        path: PathBuf,
130        /// The underlying I/O error
131        #[source]
132        source: std::io::Error,
133    },
134
135    /// Failed to create directory
136    #[error("Failed to create directory {path}: {source}")]
137    DirectoryCreation {
138        /// The directory path
139        path: PathBuf,
140        /// The underlying I/O error
141        #[source]
142        source: std::io::Error,
143    },
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_validation_error_empty_name() {
152        let err = ValidationError::EmptyName;
153        assert_eq!(err.to_string(), "Window name cannot be empty");
154    }
155
156    #[test]
157    fn test_validation_error_invalid_first_char() {
158        let err = ValidationError::InvalidFirstChar('9');
159        assert_eq!(
160            err.to_string(),
161            "Window name must start with a letter or underscore, found '9'"
162        );
163    }
164
165    #[test]
166    fn test_validation_error_invalid_characters() {
167        let err = ValidationError::InvalidCharacters;
168        assert_eq!(
169            err.to_string(),
170            "Window name contains invalid characters (only letters, numbers, and underscores allowed)"
171        );
172    }
173
174    #[test]
175    fn test_validation_error_reserved_name() {
176        let err = ValidationError::ReservedName("mod".to_string());
177        assert_eq!(err.to_string(), "'mod' is a reserved name");
178    }
179
180    #[test]
181    fn test_path_error_absolute_path() {
182        let err = PathError::AbsolutePath(PathBuf::from("/absolute/path"));
183        let msg = err.to_string();
184        assert!(msg.contains("Absolute paths are not allowed"));
185        assert!(msg.contains("/absolute/path"));
186        assert!(msg.contains("help:"));
187    }
188
189    #[test]
190    fn test_path_error_outside_project() {
191        let err = PathError::OutsideProject {
192            path: PathBuf::from("../outside"),
193            project_root: PathBuf::from("/project"),
194        };
195        let msg = err.to_string();
196        assert!(msg.contains("outside the project directory"));
197        assert!(msg.contains("help:"));
198    }
199
200    #[test]
201    fn test_project_error_cargo_toml_not_found() {
202        let err = ProjectError::CargoTomlNotFound;
203        let msg = err.to_string();
204        assert!(msg.contains("Cargo.toml not found"));
205        assert!(msg.contains("help:"));
206        assert!(msg.contains("dampen new"));
207    }
208
209    #[test]
210    fn test_project_error_not_dampen_project() {
211        let err = ProjectError::NotDampenProject;
212        let msg = err.to_string();
213        assert!(msg.contains("dampen-core dependency not found"));
214        assert!(msg.contains("help:"));
215    }
216
217    #[test]
218    fn test_generation_error_file_exists() {
219        let err = GenerationError::FileExists {
220            window_name: "settings".to_string(),
221            path: PathBuf::from("src/ui/settings.rs"),
222        };
223        let msg = err.to_string();
224        assert!(msg.contains("already exists"));
225        assert!(msg.contains("settings"));
226        assert!(msg.contains("help:"));
227    }
228
229    #[test]
230    fn test_generation_error_directory_creation() {
231        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
232        let err = GenerationError::DirectoryCreation {
233            path: PathBuf::from("src/ui/admin"),
234            source: io_err,
235        };
236        let msg = err.to_string();
237        assert!(msg.contains("Failed to create directory"));
238        assert!(msg.contains("src/ui/admin"));
239    }
240
241    #[test]
242    fn test_generation_error_file_write() {
243        let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied");
244        let err = GenerationError::FileWrite {
245            path: PathBuf::from("src/ui/settings.rs"),
246            source: io_err,
247        };
248        let msg = err.to_string();
249        assert!(msg.contains("Failed to write file"));
250        assert!(msg.contains("src/ui/settings.rs"));
251    }
252}