1use serde::Deserialize;
2use std::path::Path;
3
4#[derive(Debug, Deserialize)]
5#[serde(default)]
6pub struct FnccConfig {
7 #[serde(rename = "ui")]
8 pub ui_dir: String,
9 #[serde(rename = "lib")]
10 pub lib_dir: String,
11 #[serde(rename = "output")]
12 pub output_dir: String,
13 #[serde(rename = "entry")]
14 pub entry: String,
15}
16
17impl Default for FnccConfig {
18 fn default() -> Self {
19 Self {
20 ui_dir: "src/ui".into(),
21 lib_dir: String::new(),
22 output_dir: "target/fncc".into(),
23 entry: String::new(),
24 }
25 }
26}
27
28pub fn load(path: &Path) -> Result<FnccConfig, String> {
29 let content = std::fs::read_to_string(path).map_err(|e| format!("failed to read config: {e}"))?;
30
31 let mut config: FnccConfig = toml::from_str(&content).map_err(|e| format!("failed to parse config: {e}"))?;
32
33 if config.ui_dir.is_empty() {
34 config.ui_dir = "src/ui".into();
35 }
36 if config.output_dir.is_empty() {
37 config.output_dir = "target/fncc".into();
38 }
39
40 Ok(config)
41}
42
43#[cfg(test)]
44mod tests {
45 use super::*;
46 use std::io::Write;
47 use std::path::PathBuf;
48 use std::sync::atomic::{AtomicUsize, Ordering};
49
50 static CONFIG_TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
51
52 fn write_config(content: &str) -> PathBuf {
53 let dir = std::env::temp_dir();
54 let id = CONFIG_TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
55 let path = dir.join(format!("fncc_test_config_{}.toml", id));
56 let mut f = std::fs::File::create(&path).unwrap();
57 write!(f, "{content}").unwrap();
58 path
59 }
60
61 #[test]
64 fn test_loads_all_fields() {
65 let path = write_config("ui=\"src/myui\"\nlib=\"src/mylib\"\noutput=\"out\"\nentry=\"main\"");
66 let cfg = load(&path).unwrap();
67 assert_eq!(cfg.ui_dir, "src/myui");
68 assert_eq!(cfg.lib_dir, "src/mylib");
69 assert_eq!(cfg.output_dir, "out");
70 assert_eq!(cfg.entry, "main");
71 let _ = std::fs::remove_file(&path);
72 }
73
74 #[test]
75 fn test_loads_empty_config_with_defaults() {
76 let path = write_config("");
77 let cfg = load(&path).unwrap();
78 assert_eq!(cfg.ui_dir, "src/ui");
79 assert_eq!(cfg.output_dir, "target/fncc");
80 assert_eq!(cfg.lib_dir, "");
81 assert_eq!(cfg.entry, "");
82 let _ = std::fs::remove_file(&path);
83 }
84
85 #[test]
86 fn test_unknown_keys_ignored() {
87 let path = write_config("unknown=true\nui=\"custom\"");
88 let cfg = load(&path).unwrap();
89 assert_eq!(cfg.ui_dir, "custom");
90 let _ = std::fs::remove_file(&path);
91 }
92
93 #[test]
96 fn test_ui_dir_default_when_empty() {
97 let path = write_config("ui=\"\"");
98 let cfg = load(&path).unwrap();
99 assert_eq!(cfg.ui_dir, "src/ui");
100 let _ = std::fs::remove_file(&path);
101 }
102
103 #[test]
104 fn test_output_dir_default_when_empty() {
105 let path = write_config("output=\"\"");
106 let cfg = load(&path).unwrap();
107 assert_eq!(cfg.output_dir, "target/fncc");
108 let _ = std::fs::remove_file(&path);
109 }
110
111 #[test]
112 fn test_values_with_extra_whitespace_preserved() {
113 let path = write_config(r#"ui = " spacey ""#);
114 let cfg = load(&path).unwrap();
115 assert_eq!(cfg.ui_dir, " spacey ");
117 let _ = std::fs::remove_file(&path);
118 }
119
120 #[test]
123 fn test_nonexistent_file_returns_error() {
124 let result = load(Path::new("/tmp/__fncc_nonexistent_file_12345.toml"));
125 assert!(result.is_err());
126 }
127
128 #[test]
129 fn test_comments_are_ignored() {
130 let path = write_config("ui=\"ok\"\n# this is a comment\nlib=\"val\"");
131 let cfg = load(&path).unwrap();
132 assert_eq!(cfg.ui_dir, "ok");
133 assert_eq!(cfg.lib_dir, "val");
134 let _ = std::fs::remove_file(&path);
135 }
136}