use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct FnccConfig {
#[serde(rename = "ui")]
pub ui_dir: String,
#[serde(rename = "lib")]
pub lib_dir: String,
#[serde(rename = "output")]
pub output_dir: String,
#[serde(rename = "entry")]
pub entry: String,
}
impl Default for FnccConfig {
fn default() -> Self {
Self {
ui_dir: "src/ui".into(),
lib_dir: String::new(),
output_dir: "target/fncc".into(),
entry: String::new(),
}
}
}
pub fn load(path: &Path) -> Result<FnccConfig, String> {
let content = std::fs::read_to_string(path).map_err(|e| format!("failed to read config: {e}"))?;
let mut config: FnccConfig = toml::from_str(&content).map_err(|e| format!("failed to parse config: {e}"))?;
if config.ui_dir.is_empty() {
config.ui_dir = "src/ui".into();
}
if config.output_dir.is_empty() {
config.output_dir = "target/fncc".into();
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
static CONFIG_TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);
fn write_config(content: &str) -> PathBuf {
let dir = std::env::temp_dir();
let id = CONFIG_TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = dir.join(format!("fncc_test_config_{}.toml", id));
let mut f = std::fs::File::create(&path).unwrap();
write!(f, "{content}").unwrap();
path
}
#[test]
fn test_loads_all_fields() {
let path = write_config("ui=\"src/myui\"\nlib=\"src/mylib\"\noutput=\"out\"\nentry=\"main\"");
let cfg = load(&path).unwrap();
assert_eq!(cfg.ui_dir, "src/myui");
assert_eq!(cfg.lib_dir, "src/mylib");
assert_eq!(cfg.output_dir, "out");
assert_eq!(cfg.entry, "main");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_loads_empty_config_with_defaults() {
let path = write_config("");
let cfg = load(&path).unwrap();
assert_eq!(cfg.ui_dir, "src/ui");
assert_eq!(cfg.output_dir, "target/fncc");
assert_eq!(cfg.lib_dir, "");
assert_eq!(cfg.entry, "");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_unknown_keys_ignored() {
let path = write_config("unknown=true\nui=\"custom\"");
let cfg = load(&path).unwrap();
assert_eq!(cfg.ui_dir, "custom");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_ui_dir_default_when_empty() {
let path = write_config("ui=\"\"");
let cfg = load(&path).unwrap();
assert_eq!(cfg.ui_dir, "src/ui");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_output_dir_default_when_empty() {
let path = write_config("output=\"\"");
let cfg = load(&path).unwrap();
assert_eq!(cfg.output_dir, "target/fncc");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_values_with_extra_whitespace_preserved() {
let path = write_config(r#"ui = " spacey ""#);
let cfg = load(&path).unwrap();
assert_eq!(cfg.ui_dir, " spacey ");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_nonexistent_file_returns_error() {
let result = load(Path::new("/tmp/__fncc_nonexistent_file_12345.toml"));
assert!(result.is_err());
}
#[test]
fn test_comments_are_ignored() {
let path = write_config("ui=\"ok\"\n# this is a comment\nlib=\"val\"");
let cfg = load(&path).unwrap();
assert_eq!(cfg.ui_dir, "ok");
assert_eq!(cfg.lib_dir, "val");
let _ = std::fs::remove_file(&path);
}
}