use std::fs;
use std::path::{Path, PathBuf};
use crate::config::schema::FileConfig;
#[derive(Debug)]
pub enum LoadError {
Io {
path: PathBuf,
source: std::io::Error,
},
Toml {
path: PathBuf,
source: toml::de::Error,
},
}
impl std::fmt::Display for LoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Io {
ref path,
ref source,
} => write!(f, "failed to read {}: {source}", path.display()),
Self::Toml {
ref path,
ref source,
} => write!(f, "failed to parse {}: {source}", path.display()),
}
}
}
impl std::error::Error for LoadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
Self::Io { ref source, .. } => Some(source),
Self::Toml { ref source, .. } => Some(source),
}
}
}
pub fn load_config(project_dir: &Path) -> Result<FileConfig, LoadError> {
let path = project_dir.join("kaze.toml");
let s = fs::read_to_string(&path).map_err(|e| LoadError::Io {
path: path.clone(),
source: e,
})?;
let value = toml::from_str::<toml::Value>(&s).map_err(|e| LoadError::Toml {
path: path.clone(),
source: e,
})?;
if value.get("args").is_some() {
return Err(LoadError::Toml {
path,
source: <toml::de::Error as serde::de::Error>::custom(
"top-level [args] is no longer supported; move args under [project.args]",
),
});
}
toml::from_str::<FileConfig>(&s).map_err(|e| LoadError::Toml { path, source: e })
}
#[cfg(test)]
mod tests {
use std::error::Error;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
fn make_temp_dir(label: &str) -> PathBuf {
static COUNTER: AtomicUsize = AtomicUsize::new(0);
let mut dir = std::env::temp_dir();
dir.push(format!(
"nishikaze-test-{}-{}",
label,
COUNTER.fetch_add(1, Ordering::Relaxed)
));
if dir.exists() {
fs::remove_dir_all(&dir).expect("clean temp dir");
}
fs::create_dir_all(&dir).expect("create temp dir");
dir
}
#[test]
fn load_config_success() {
let dir = make_temp_dir("load-ok");
let path = dir.join("kaze.toml");
fs::write(&path, "").expect("write config");
let cfg = load_config(&dir).expect("load config");
assert_eq!(cfg.project.board, None);
}
#[test]
fn load_config_missing_file() {
let dir = make_temp_dir("load-missing");
let err = load_config(&dir).expect_err("missing file");
assert!(matches!(err, LoadError::Io { .. }));
}
#[test]
fn load_config_invalid_toml() {
let dir = make_temp_dir("load-invalid");
let path = dir.join("kaze.toml");
fs::write(&path, "not = [").expect("write config");
let err = load_config(&dir).expect_err("invalid toml");
assert!(matches!(err, LoadError::Toml { .. }));
}
#[test]
fn load_error_display_includes_path() {
let dir = make_temp_dir("load-display");
let err = load_config(&dir).expect_err("missing file");
let msg = err.to_string();
assert!(msg.contains("kaze.toml"));
}
#[test]
fn load_error_sources_are_exposed() {
let dir_io = make_temp_dir("load-source-io");
let err_io = load_config(&dir_io).expect_err("missing file");
assert!(err_io.source().is_some());
let dir_toml = make_temp_dir("load-source-toml");
let path = dir_toml.join("kaze.toml");
fs::write(&path, "not = [").expect("write config");
let err_toml = load_config(&dir_toml).expect_err("invalid toml");
assert!(err_toml.source().is_some());
}
#[test]
fn load_config_rejects_legacy_args() {
let dir = make_temp_dir("load-legacy-args");
let path = dir.join("kaze.toml");
fs::write(&path, "[args]\nconf = [\"-DOLD=1\"]\n").expect("write config");
let err = load_config(&dir).expect_err("legacy args rejected");
let msg = err.to_string();
assert!(msg.contains("top-level [args] is no longer supported"));
}
#[test]
fn load_config_parses_project_fields() {
let dir = make_temp_dir("load-parse");
let path = dir.join("kaze.toml");
fs::write(
&path,
r#"[project]
board = "native_sim"
runner = "openocd"
name = "demo"
"#,
)
.expect("write config");
let cfg = load_config(&dir).expect("load config");
assert_eq!(cfg.project.board.as_deref(), Some("native_sim"));
assert_eq!(cfg.project.runner.as_deref(), Some("openocd"));
assert_eq!(cfg.project.name.as_deref(), Some("demo"));
}
}