cargo_hammerwork/commands/
config.rs1use 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 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 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 let limit = config.get_default_limit().to_string();
84 table.add_row(vec!["default_limit", &limit, "Config File"]);
85
86 let log_level = config.get_log_level();
88 table.add_row(vec!["log_level", log_level, "Config File"]);
89
90 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 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 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 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 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 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}