Skip to main content

cargo_hammerwork/commands/
config.rs

1use anyhow::Result;
2use clap::Subcommand;
3use tracing::info;
4
5use crate::config::Config;
6
7#[derive(Subcommand)]
8pub enum ConfigCommand {
9    #[command(about = "Show current configuration")]
10    Show,
11    #[command(about = "Set a configuration value")]
12    Set {
13        #[arg(help = "Configuration key")]
14        key: String,
15        #[arg(help = "Configuration value")]
16        value: String,
17    },
18    #[command(about = "Get a configuration value")]
19    Get {
20        #[arg(help = "Configuration key")]
21        key: String,
22    },
23    #[command(about = "Reset configuration to defaults")]
24    Reset {
25        #[arg(long, help = "Confirm the reset operation")]
26        confirm: bool,
27    },
28    #[command(about = "Show configuration file path")]
29    Path,
30}
31
32impl ConfigCommand {
33    pub async fn execute(&self, config: &mut Config) -> Result<()> {
34        match self {
35            ConfigCommand::Show => {
36                show_config(config).await?;
37            }
38            ConfigCommand::Set { key, value } => {
39                set_config_value(config, key, value).await?;
40            }
41            ConfigCommand::Get { key } => {
42                get_config_value(config, key).await?;
43            }
44            ConfigCommand::Reset { confirm } => {
45                reset_config(config, *confirm).await?;
46            }
47            ConfigCommand::Path => {
48                show_config_path().await?;
49            }
50        }
51        Ok(())
52    }
53}
54
55async fn show_config(config: &Config) -> Result<()> {
56    println!("⚙️  Hammerwork Configuration");
57    println!("═══════════════════════════");
58
59    let mut table = comfy_table::Table::new();
60    table.set_header(vec!["Setting", "Value", "Source"]);
61
62    // Database URL
63    let (db_url, db_source) = if let Some(url) = &config.database_url {
64        (url.clone(), "Config File")
65    } else if let Ok(env_url) = std::env::var("DATABASE_URL") {
66        (env_url, "Environment")
67    } else {
68        ("Not set".to_string(), "Default")
69    };
70    table.add_row(vec!["database_url", &db_url, db_source]);
71
72    // Default queue
73    let (queue, queue_source) = if let Some(q) = &config.default_queue {
74        (q.clone(), "Config File")
75    } else if let Ok(env_queue) = std::env::var("HAMMERWORK_DEFAULT_QUEUE") {
76        (env_queue, "Environment")
77    } else {
78        ("Not set".to_string(), "Default")
79    };
80    table.add_row(vec!["default_queue", &queue, queue_source]);
81
82    // Default limit
83    let limit = config.get_default_limit().to_string();
84    table.add_row(vec!["default_limit", &limit, "Config File"]);
85
86    // Log level
87    let log_level = config.get_log_level();
88    table.add_row(vec!["log_level", log_level, "Config File"]);
89
90    // Connection pool size
91    let pool_size = config.get_connection_pool_size().to_string();
92    table.add_row(vec!["connection_pool_size", &pool_size, "Config File"]);
93
94    println!("{}", table);
95
96    println!("\n💡 Configuration priority: Environment Variables > Config File > Defaults");
97    println!("📝 Use 'config set <key> <value>' to update configuration");
98
99    Ok(())
100}
101
102async fn set_config_value(config: &mut Config, key: &str, value: &str) -> Result<()> {
103    match key {
104        "database_url" => {
105            crate::utils::validation::validate_database_url(value)?;
106            config.database_url = Some(value.to_string());
107            info!("✅ Set database_url");
108        }
109        "default_queue" => {
110            config.default_queue = Some(value.to_string());
111            info!("✅ Set default_queue to: {}", value);
112        }
113        "default_limit" => {
114            let limit: u32 = value
115                .parse()
116                .map_err(|_| anyhow::anyhow!("default_limit must be a positive integer"))?;
117            config.default_limit = Some(limit);
118            info!("✅ Set default_limit to: {}", limit);
119        }
120        "log_level" => {
121            if !["trace", "debug", "info", "warn", "error"].contains(&value.to_lowercase().as_str())
122            {
123                return Err(anyhow::anyhow!(
124                    "log_level must be one of: trace, debug, info, warn, error"
125                ));
126            }
127            config.log_level = Some(value.to_lowercase());
128            info!("✅ Set log_level to: {}", value);
129        }
130        "connection_pool_size" => {
131            let size: u32 = value
132                .parse()
133                .map_err(|_| anyhow::anyhow!("connection_pool_size must be a positive integer"))?;
134            if size == 0 || size > 100 {
135                return Err(anyhow::anyhow!(
136                    "connection_pool_size must be between 1 and 100"
137                ));
138            }
139            config.connection_pool_size = Some(size);
140            info!("✅ Set connection_pool_size to: {}", size);
141        }
142        _ => {
143            return Err(anyhow::anyhow!(
144                "Unknown configuration key: {}. Valid keys: database_url, default_queue, default_limit, log_level, connection_pool_size",
145                key
146            ));
147        }
148    }
149
150    // Save the updated configuration
151    config.save()?;
152    println!("💾 Configuration saved");
153
154    Ok(())
155}
156
157async fn get_config_value(config: &Config, key: &str) -> Result<()> {
158    let value = match key {
159        "database_url" => config.get_database_url().unwrap_or("Not set").to_string(),
160        "default_queue" => config.get_default_queue().unwrap_or("Not set").to_string(),
161        "default_limit" => config.get_default_limit().to_string(),
162        "log_level" => config.get_log_level().to_string(),
163        "connection_pool_size" => config.get_connection_pool_size().to_string(),
164        _ => {
165            return Err(anyhow::anyhow!(
166                "Unknown configuration key: {}. Valid keys: database_url, default_queue, default_limit, log_level, connection_pool_size",
167                key
168            ));
169        }
170    };
171
172    println!("{}", value);
173    Ok(())
174}
175
176async fn reset_config(config: &mut Config, confirm: bool) -> Result<()> {
177    if !confirm {
178        println!("⚠️  This will reset all configuration to defaults. Use --confirm to proceed.");
179        return Ok(());
180    }
181
182    *config = Config::default();
183    config.save()?;
184
185    println!("🔄 Configuration reset to defaults");
186    info!("Configuration has been reset to defaults");
187
188    Ok(())
189}
190
191async fn show_config_path() -> Result<()> {
192    let config_dir = dirs::config_dir()
193        .or_else(dirs::home_dir)
194        .ok_or_else(|| anyhow::anyhow!("Cannot find config directory"))?;
195
196    let config_path = config_dir.join("hammerwork").join("config.toml");
197
198    println!("📁 Configuration file path:");
199    println!("{}", config_path.display());
200
201    if config_path.exists() {
202        println!("✅ File exists");
203
204        // Show file size and modification time
205        let metadata = std::fs::metadata(&config_path)?;
206        let size = metadata.len();
207        let modified = metadata.modified()?;
208        let modified_time = chrono::DateTime::<chrono::Utc>::from(modified);
209
210        println!("📊 Size: {} bytes", size);
211        println!(
212            "📅 Last modified: {}",
213            modified_time.format("%Y-%m-%d %H:%M:%S UTC")
214        );
215    } else {
216        println!("❌ File does not exist (will be created when configuration is saved)");
217    }
218
219    Ok(())
220}
221
222#[cfg(test)]
223mod tests {
224
225    #[test]
226    fn test_config_command_structure() {
227        let commands = vec!["show", "set", "get", "reset", "path"];
228
229        for cmd in commands {
230            assert!(!cmd.is_empty());
231            assert!(cmd.chars().all(|c| c.is_ascii_lowercase()));
232        }
233    }
234
235    #[test]
236    fn test_config_key_validation() {
237        let valid_keys = vec![
238            "database_url",
239            "default_queue",
240            "default_limit",
241            "log_level",
242            "connection_pool_size",
243        ];
244
245        for key in valid_keys {
246            assert!(is_valid_config_key(key));
247        }
248
249        let invalid_keys = vec!["", "invalid_key", "key with spaces", "key/with/slashes"];
250
251        for key in invalid_keys {
252            assert!(!is_valid_config_key(key));
253        }
254    }
255
256    #[test]
257    fn test_config_value_validation() {
258        // Test database URL values
259        assert!(is_valid_config_value(
260            "database_url",
261            "postgres://localhost/db"
262        ));
263        assert!(!is_valid_config_value("database_url", "invalid_url"));
264
265        // Test log level values
266        assert!(is_valid_config_value("log_level", "info"));
267        assert!(is_valid_config_value("log_level", "debug"));
268        assert!(!is_valid_config_value("log_level", "invalid"));
269
270        // Test numeric values
271        assert!(is_valid_config_value("default_limit", "100"));
272        assert!(is_valid_config_value("connection_pool_size", "5"));
273        assert!(!is_valid_config_value("default_limit", "not_a_number"));
274    }
275
276    fn is_valid_config_key(key: &str) -> bool {
277        matches!(
278            key,
279            "database_url"
280                | "default_queue"
281                | "default_limit"
282                | "log_level"
283                | "connection_pool_size"
284        )
285    }
286
287    fn is_valid_config_value(key: &str, value: &str) -> bool {
288        match key {
289            "database_url" => {
290                !value.is_empty()
291                    && (value.starts_with("postgres://") || value.starts_with("mysql://"))
292            }
293            "log_level" => matches!(value, "error" | "warn" | "info" | "debug" | "trace"),
294            "default_limit" | "connection_pool_size" => value.parse::<u32>().is_ok(),
295            "default_queue" => !value.is_empty() && !value.contains(' '),
296            _ => false,
297        }
298    }
299}