python_proto_importer/commands/
clean.rs

1use crate::config::AppConfig;
2use anyhow::{Context, Result, bail};
3use std::fs;
4use std::path::Path;
5
6/// Execute the clean command to remove the generated output directory.
7///
8/// This command removes the entire output directory and all its contents, as
9/// specified in the pyproject.toml configuration. It provides a safety mechanism
10/// by requiring explicit confirmation to prevent accidental deletion.
11///
12/// # Arguments
13///
14/// * `pyproject` - Optional path to the pyproject.toml file. If None, uses "pyproject.toml"
15/// * `yes` - Safety flag that must be true to actually perform the deletion
16///
17/// # Returns
18///
19/// Returns `Ok(())` if the operation completes successfully, or an error if:
20/// - Configuration cannot be loaded
21/// - The safety flag (`yes`) is false when the directory exists
22/// - Directory removal fails due to permissions or other filesystem issues
23///
24/// # Safety Features
25///
26/// - **Confirmation Required**: Refuses to delete without explicit `yes` flag
27/// - **No-op for Missing**: Succeeds silently if the output directory doesn't exist
28/// - **Complete Removal**: Recursively removes all files and subdirectories
29///
30/// # Use Cases
31///
32/// - **Fresh Start**: Clear all generated code before regeneration
33/// - **CI Cleanup**: Ensure clean environment between builds
34/// - **Development**: Reset state during iteration
35///
36/// # Example
37///
38/// ```no_run
39/// use python_proto_importer::commands::clean;
40///
41/// // Safe call - will refuse to delete without confirmation
42/// let result = clean(None, false);
43/// assert!(result.is_err()); // Expects error without --yes
44///
45/// // Actual deletion with confirmation
46/// clean(None, true)?; // Removes the configured output directory
47/// # Ok::<(), anyhow::Error>(())
48/// ```
49pub fn clean(pyproject: Option<&str>, yes: bool) -> Result<()> {
50    let cfg = AppConfig::load(pyproject.map(Path::new)).context("failed to load config")?;
51    let out = &cfg.out;
52    if out.exists() {
53        if !yes {
54            bail!("refusing to remove {} without --yes", out.display());
55        }
56        tracing::info!("removing {}", out.display());
57        fs::remove_dir_all(out).with_context(|| format!("failed to remove {}", out.display()))?;
58    }
59    Ok(())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::fs;
66    use std::io::Write;
67    use tempfile::TempDir;
68
69    fn create_test_config_file(dir: &Path, out_dir: &str) -> Result<String> {
70        let config_file = dir.join("pyproject.toml");
71        let mut file = fs::File::create(&config_file)?;
72        writeln!(file, "[tool.python_proto_importer]")?;
73        writeln!(file, "out = \"{}\"", out_dir)?;
74        writeln!(file, "proto_path = [\"proto\"]")?;
75        writeln!(file, "python_exe = \"python3\"")?;
76        Ok(config_file.to_string_lossy().to_string())
77    }
78
79    #[test]
80    fn test_clean_without_yes_flag() {
81        let temp_dir = TempDir::new().unwrap();
82        let out_dir = temp_dir.path().join("output");
83        fs::create_dir(&out_dir).unwrap();
84
85        let config_file =
86            create_test_config_file(temp_dir.path(), &out_dir.to_string_lossy()).unwrap();
87
88        let result = clean(Some(&config_file), false);
89
90        assert!(result.is_err());
91        assert!(
92            result
93                .unwrap_err()
94                .to_string()
95                .contains("refusing to remove")
96        );
97        assert!(out_dir.exists()); // Directory should still exist
98    }
99
100    #[test]
101    fn test_clean_with_yes_flag() {
102        let temp_dir = TempDir::new().unwrap();
103        let out_dir = temp_dir.path().join("output");
104        fs::create_dir(&out_dir).unwrap();
105
106        let config_file =
107            create_test_config_file(temp_dir.path(), &out_dir.to_string_lossy()).unwrap();
108
109        let result = clean(Some(&config_file), true);
110
111        assert!(result.is_ok());
112        assert!(!out_dir.exists()); // Directory should be removed
113    }
114
115    #[test]
116    fn test_clean_nonexistent_directory() {
117        let temp_dir = TempDir::new().unwrap();
118        let out_dir = temp_dir.path().join("nonexistent");
119
120        let config_file =
121            create_test_config_file(temp_dir.path(), &out_dir.to_string_lossy()).unwrap();
122
123        let result = clean(Some(&config_file), true);
124
125        // Should succeed even if directory doesn't exist
126        assert!(result.is_ok());
127    }
128
129    #[test]
130    fn test_clean_with_files_in_directory() {
131        let temp_dir = TempDir::new().unwrap();
132        let out_dir = temp_dir.path().join("output");
133        fs::create_dir(&out_dir).unwrap();
134
135        // Create some files in the output directory
136        fs::write(out_dir.join("test_file.py"), "# test content").unwrap();
137        let subdir = out_dir.join("subdir");
138        fs::create_dir(&subdir).unwrap();
139        fs::write(subdir.join("another_file.py"), "# more content").unwrap();
140
141        let config_file =
142            create_test_config_file(temp_dir.path(), &out_dir.to_string_lossy()).unwrap();
143
144        let result = clean(Some(&config_file), true);
145
146        assert!(result.is_ok());
147        assert!(!out_dir.exists()); // Directory and all contents should be removed
148    }
149}