1use crate::cli::ConfigAction;
10use crate::config::FabrykConfig;
11use fabryk_core::traits::ConfigManager;
12use fabryk_core::{Error, Result};
13use std::path::PathBuf;
14
15pub fn handle_config_command(config_path: Option<&str>, action: ConfigAction) -> Result<()> {
24 match action {
25 ConfigAction::Path => cmd_config_path::<FabrykConfig>(config_path),
26 ConfigAction::Get { key } => cmd_config_get::<FabrykConfig>(config_path, &key),
27 ConfigAction::Set { key, value } => {
28 cmd_config_set::<FabrykConfig>(config_path, &key, &value)
29 }
30 ConfigAction::Init { file, force } => {
31 cmd_config_init::<FabrykConfig>(file.as_deref(), force)
32 }
33 ConfigAction::Export { docker_env } => {
34 let config = FabrykConfig::load(config_path)?;
35 cmd_config_export(&config, docker_env)
36 }
37 }
38}
39
40pub fn cmd_config_path<C: ConfigManager>(config_path: Option<&str>) -> Result<()> {
46 match C::resolve_config_path(config_path) {
47 Some(path) => {
48 let exists = path.exists();
49 println!("{}", path.display());
50 if !exists {
51 eprintln!(
52 "(file does not exist — run `{} config init` to create it)",
53 C::project_name()
54 );
55 }
56 Ok(())
57 }
58 None => Err(Error::config(
59 "Could not determine config directory for this platform",
60 )),
61 }
62}
63
64pub fn cmd_config_get<C: ConfigManager>(config_path: Option<&str>, key: &str) -> Result<()> {
66 let config = C::load(config_path)?;
67 let value = toml::Value::try_from(&config).map_err(|e| Error::config(e.to_string()))?;
68 match get_nested_value(&value, key) {
69 Some(val) => {
70 println!("{}", format_toml_value(val));
71 Ok(())
72 }
73 None => Err(Error::config(format!(
74 "Key '{key}' not found in configuration"
75 ))),
76 }
77}
78
79pub fn cmd_config_set<C: ConfigManager>(
81 config_path: Option<&str>,
82 key: &str,
83 value: &str,
84) -> Result<()> {
85 let path = C::resolve_config_path(config_path)
86 .ok_or_else(|| Error::config("Could not determine config directory"))?;
87
88 let mut doc: toml::Value = if path.exists() {
89 let content = std::fs::read_to_string(&path).map_err(|e| Error::io_with_path(e, &path))?;
90 toml::from_str(&content)
91 .map_err(|e| Error::config(format!("Failed to parse {}: {e}", path.display())))?
92 } else {
93 return Err(Error::config(format!(
94 "Config file does not exist at {}. Run `{} config init` first.",
95 path.display(),
96 C::project_name()
97 )));
98 };
99
100 set_nested_value(&mut doc, key, parse_value(value))?;
101
102 let toml_str = toml::to_string_pretty(&doc).map_err(|e| Error::config(e.to_string()))?;
103 std::fs::write(&path, toml_str).map_err(|e| Error::io_with_path(e, &path))?;
104
105 println!("Set {key} = {value} in {}", path.display());
106 Ok(())
107}
108
109pub fn cmd_config_init<C: ConfigManager>(file: Option<&str>, force: bool) -> Result<()> {
111 let path = match file {
112 Some(p) => PathBuf::from(p),
113 None => C::default_config_path()
114 .ok_or_else(|| Error::config("Could not determine config directory"))?,
115 };
116
117 if path.exists() && !force {
118 return Err(Error::config(format!(
119 "Config file already exists at {}. Use --force to overwrite.",
120 path.display()
121 )));
122 }
123
124 if let Some(parent) = path.parent() {
125 std::fs::create_dir_all(parent).map_err(|e| Error::io_with_path(e, parent))?;
126 }
127
128 let config = C::default();
129 let toml_str = config.to_toml_string()?;
130 std::fs::write(&path, &toml_str).map_err(|e| Error::io_with_path(e, &path))?;
131
132 println!("Config file created at {}", path.display());
133 Ok(())
134}
135
136pub fn cmd_config_export<C: ConfigManager>(config: &C, docker_env: bool) -> Result<()> {
138 let vars = config.to_env_vars()?;
139 for (key, value) in &vars {
140 if docker_env {
141 println!("--env {key}={value}");
142 } else {
143 println!("{key}={value}");
144 }
145 }
146 Ok(())
147}
148
149pub fn get_nested_value<'a>(value: &'a toml::Value, key: &str) -> Option<&'a toml::Value> {
155 let parts: Vec<&str> = key.split('.').collect();
156 let mut current = value;
157 for part in &parts {
158 current = current.as_table()?.get(*part)?;
159 }
160 Some(current)
161}
162
163pub fn set_nested_value(root: &mut toml::Value, key: &str, value: toml::Value) -> Result<()> {
165 let parts: Vec<&str> = key.split('.').collect();
166 let mut current = root;
167
168 for (i, part) in parts.iter().enumerate() {
169 if i == parts.len() - 1 {
170 let table = current
171 .as_table_mut()
172 .ok_or_else(|| Error::config("Cannot set key on a non-table value"))?;
173 table.insert(part.to_string(), value);
174 return Ok(());
175 }
176
177 let table = current
178 .as_table_mut()
179 .ok_or_else(|| Error::config("Cannot navigate into a non-table value"))?;
180 if !table.contains_key(*part) {
181 table.insert(part.to_string(), toml::Value::Table(toml::map::Map::new()));
182 }
183 current = table.get_mut(*part).unwrap();
184 }
185
186 Err(Error::config("Empty key path"))
187}
188
189pub fn parse_value(s: &str) -> toml::Value {
193 if s == "true" {
194 return toml::Value::Boolean(true);
195 }
196 if s == "false" {
197 return toml::Value::Boolean(false);
198 }
199 if let Ok(i) = s.parse::<i64>() {
200 return toml::Value::Integer(i);
201 }
202 if let Ok(f) = s.parse::<f64>() {
203 return toml::Value::Float(f);
204 }
205 toml::Value::String(s.to_string())
206}
207
208pub fn format_toml_value(value: &toml::Value) -> String {
210 match value {
211 toml::Value::String(s) => s.clone(),
212 toml::Value::Integer(i) => i.to_string(),
213 toml::Value::Float(f) => f.to_string(),
214 toml::Value::Boolean(b) => b.to_string(),
215 toml::Value::Datetime(dt) => dt.to_string(),
216 toml::Value::Array(_) | toml::Value::Table(_) => {
217 toml::to_string_pretty(value).unwrap_or_else(|_| format!("{value:?}"))
218 }
219 }
220}
221
222#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
235 fn test_cmd_config_path_default() {
236 let result = cmd_config_path::<FabrykConfig>(None);
237 assert!(result.is_ok());
238 }
239
240 #[test]
241 fn test_cmd_config_path_explicit() {
242 let result = cmd_config_path::<FabrykConfig>(Some("/explicit/config.toml"));
243 assert!(result.is_ok());
244 }
245
246 #[test]
251 fn test_cmd_config_get_simple_key() {
252 let dir = tempfile::TempDir::new().unwrap();
253 let path = dir.path().join("config.toml");
254 let config = FabrykConfig::default();
255 std::fs::write(&path, config.to_toml_string().unwrap()).unwrap();
256
257 let result = cmd_config_get::<FabrykConfig>(Some(path.to_str().unwrap()), "project_name");
258 assert!(result.is_ok());
259 }
260
261 #[test]
262 fn test_cmd_config_get_nested_key() {
263 let dir = tempfile::TempDir::new().unwrap();
264 let path = dir.path().join("config.toml");
265 let config = FabrykConfig::default();
266 std::fs::write(&path, config.to_toml_string().unwrap()).unwrap();
267
268 let result = cmd_config_get::<FabrykConfig>(Some(path.to_str().unwrap()), "server.port");
269 assert!(result.is_ok());
270 }
271
272 #[test]
273 fn test_cmd_config_get_missing_key() {
274 let dir = tempfile::TempDir::new().unwrap();
275 let path = dir.path().join("config.toml");
276 let config = FabrykConfig::default();
277 std::fs::write(&path, config.to_toml_string().unwrap()).unwrap();
278
279 let result =
280 cmd_config_get::<FabrykConfig>(Some(path.to_str().unwrap()), "nonexistent.key");
281 assert!(result.is_err());
282 assert!(result.unwrap_err().to_string().contains("not found"));
283 }
284
285 #[test]
290 fn test_cmd_config_set_simple_key() {
291 let dir = tempfile::TempDir::new().unwrap();
292 let path = dir.path().join("config.toml");
293 let config = FabrykConfig::default();
294 std::fs::write(&path, config.to_toml_string().unwrap()).unwrap();
295
296 let result = cmd_config_set::<FabrykConfig>(
297 Some(path.to_str().unwrap()),
298 "project_name",
299 "new-name",
300 );
301 assert!(result.is_ok());
302
303 let content = std::fs::read_to_string(&path).unwrap();
304 assert!(content.contains("new-name"));
305 }
306
307 #[test]
308 fn test_cmd_config_set_nested_key() {
309 let dir = tempfile::TempDir::new().unwrap();
310 let path = dir.path().join("config.toml");
311 let config = FabrykConfig::default();
312 std::fs::write(&path, config.to_toml_string().unwrap()).unwrap();
313
314 let result =
315 cmd_config_set::<FabrykConfig>(Some(path.to_str().unwrap()), "server.port", "8080");
316 assert!(result.is_ok());
317
318 let content = std::fs::read_to_string(&path).unwrap();
319 assert!(content.contains("8080"));
320 }
321
322 #[test]
323 fn test_cmd_config_set_missing_file() {
324 let result =
325 cmd_config_set::<FabrykConfig>(Some("/nonexistent/config.toml"), "key", "value");
326 assert!(result.is_err());
327 assert!(result.unwrap_err().to_string().contains("does not exist"));
328 }
329
330 #[test]
335 fn test_cmd_config_init_creates_file() {
336 let dir = tempfile::TempDir::new().unwrap();
337 let path = dir.path().join("fabryk").join("config.toml");
338
339 let result = cmd_config_init::<FabrykConfig>(Some(path.to_str().unwrap()), false);
340 assert!(result.is_ok());
341 assert!(path.exists());
342
343 let content = std::fs::read_to_string(&path).unwrap();
344 assert!(content.contains("project_name"));
345 assert!(content.contains("[server]"));
346 }
347
348 #[test]
349 fn test_cmd_config_init_no_overwrite() {
350 let dir = tempfile::TempDir::new().unwrap();
351 let path = dir.path().join("config.toml");
352 std::fs::write(&path, "existing").unwrap();
353
354 let result = cmd_config_init::<FabrykConfig>(Some(path.to_str().unwrap()), false);
355 assert!(result.is_err());
356 assert!(result.unwrap_err().to_string().contains("already exists"));
357 }
358
359 #[test]
360 fn test_cmd_config_init_force_overwrites() {
361 let dir = tempfile::TempDir::new().unwrap();
362 let path = dir.path().join("config.toml");
363 std::fs::write(&path, "old content").unwrap();
364
365 let result = cmd_config_init::<FabrykConfig>(Some(path.to_str().unwrap()), true);
366 assert!(result.is_ok());
367
368 let content = std::fs::read_to_string(&path).unwrap();
369 assert!(content.contains("project_name"));
370 }
371
372 #[test]
377 fn test_cmd_config_export_env_vars() {
378 let config = FabrykConfig::default();
379 let result = cmd_config_export(&config, false);
380 assert!(result.is_ok());
381 }
382
383 #[test]
384 fn test_cmd_config_export_docker_env() {
385 let config = FabrykConfig::default();
386 let result = cmd_config_export(&config, true);
387 assert!(result.is_ok());
388 }
389
390 #[test]
395 fn test_get_nested_value_top_level() {
396 let val: toml::Value = toml::from_str("port = 8080").unwrap();
397 let result = get_nested_value(&val, "port");
398 assert_eq!(result, Some(&toml::Value::Integer(8080)));
399 }
400
401 #[test]
402 fn test_get_nested_value_nested() {
403 let val: toml::Value = toml::from_str("[server]\nport = 3000").unwrap();
404 let result = get_nested_value(&val, "server.port");
405 assert_eq!(result, Some(&toml::Value::Integer(3000)));
406 }
407
408 #[test]
409 fn test_get_nested_value_missing() {
410 let val: toml::Value = toml::from_str("port = 8080").unwrap();
411 assert!(get_nested_value(&val, "nonexistent").is_none());
412 }
413
414 #[test]
415 fn test_get_nested_value_deep_missing() {
416 let val: toml::Value = toml::from_str("[server]\nport = 3000").unwrap();
417 assert!(get_nested_value(&val, "server.nonexistent").is_none());
418 }
419
420 #[test]
425 fn test_set_nested_value_top_level() {
426 let mut val: toml::Value = toml::from_str("port = 8080").unwrap();
427 set_nested_value(&mut val, "port", toml::Value::Integer(9090)).unwrap();
428 assert_eq!(
429 get_nested_value(&val, "port"),
430 Some(&toml::Value::Integer(9090))
431 );
432 }
433
434 #[test]
435 fn test_set_nested_value_creates_section() {
436 let mut val = toml::Value::Table(toml::map::Map::new());
437 set_nested_value(&mut val, "server.port", toml::Value::Integer(3000)).unwrap();
438 assert_eq!(
439 get_nested_value(&val, "server.port"),
440 Some(&toml::Value::Integer(3000))
441 );
442 }
443
444 #[test]
445 fn test_set_nested_value_overwrites() {
446 let mut val: toml::Value = toml::from_str("[server]\nport = 3000").unwrap();
447 set_nested_value(&mut val, "server.port", toml::Value::Integer(8080)).unwrap();
448 assert_eq!(
449 get_nested_value(&val, "server.port"),
450 Some(&toml::Value::Integer(8080))
451 );
452 }
453
454 #[test]
459 fn test_parse_value_types() {
460 assert_eq!(parse_value("true"), toml::Value::Boolean(true));
461 assert_eq!(parse_value("false"), toml::Value::Boolean(false));
462 assert_eq!(parse_value("42"), toml::Value::Integer(42));
463 assert_eq!(parse_value("-7"), toml::Value::Integer(-7));
464 assert_eq!(parse_value("3.14"), toml::Value::Float(3.14));
465 assert_eq!(
466 parse_value("hello world"),
467 toml::Value::String("hello world".to_string())
468 );
469 }
470
471 #[test]
476 fn test_format_toml_value() {
477 assert_eq!(
478 format_toml_value(&toml::Value::String("hello".into())),
479 "hello"
480 );
481 assert_eq!(format_toml_value(&toml::Value::Integer(42)), "42");
482 assert_eq!(format_toml_value(&toml::Value::Float(3.14)), "3.14");
483 assert_eq!(format_toml_value(&toml::Value::Boolean(true)), "true");
484 }
485}