use std::fs;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileConfig {
pub template: Option<String>,
pub output: Option<PathBuf>,
pub jobs: Option<usize>,
pub folder: Option<bool>,
pub no_tag: Option<bool>,
pub on_conflict: Option<String>,
pub recursive: Option<bool>,
#[allow(dead_code)]
pub lang: Option<String>,
pub format: Option<Vec<String>>,
}
pub fn default_path() -> Option<PathBuf> {
dirs::config_dir().map(|dir| dir.join("ncm2mp3").join("config.toml"))
}
pub fn load_from(path: &Path) -> Result<FileConfig> {
let text = fs::read_to_string(path)
.with_context(|| format!("failed to read config file: {}", path.display()))?;
toml::from_str(&text)
.with_context(|| format!("failed to parse config file: {}", path.display()))
}
pub fn load_default_if_present() -> Result<FileConfig> {
match default_path() {
Some(p) if p.exists() => load_from(&p),
_ => Ok(FileConfig::default()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_all_fields() {
let toml_src = r#"
template = "{artist}/{album}/{title}"
output = "./out"
jobs = 4
folder = true
no_tag = false
on_conflict = "rename"
recursive = true
lang = "zh"
format = ["mp3", "flac"]
"#;
let cfg: FileConfig = toml::from_str(toml_src).unwrap();
assert_eq!(cfg.template.as_deref(), Some("{artist}/{album}/{title}"));
assert_eq!(cfg.jobs, Some(4));
assert_eq!(cfg.folder, Some(true));
assert_eq!(cfg.on_conflict.as_deref(), Some("rename"));
assert_eq!(cfg.lang.as_deref(), Some("zh"));
assert_eq!(
cfg.format.as_deref(),
Some(&["mp3".to_string(), "flac".to_string()][..])
);
}
#[test]
fn partial_config_keeps_other_fields_none() {
let cfg: FileConfig = toml::from_str(r#"jobs = 2"#).unwrap();
assert_eq!(cfg.jobs, Some(2));
assert!(cfg.template.is_none());
assert!(cfg.folder.is_none());
}
#[test]
fn unknown_field_is_rejected() {
let result: Result<FileConfig, _> = toml::from_str(r#"templte = "oops""#);
assert!(result.is_err());
}
}