diff --git a/src/config.rs b/src/config.rs index b6c37bd..4f58007 100644 --- a/src/config.rs +++ b/src/config.rs @@ -169,6 +169,20 @@ pub fn parse(text: &str) -> Result<(Config, Vec)> { Ok((config, unknown)) } +/// Checks a config file strictly, for home-manager's build: invalid values +/// and unknown keys are errors (at run time unknown keys only warn). +pub fn check(path: &Path) -> Result<()> { + let text = std::fs::read_to_string(path) + .with_context(|| format!("could not read {}", path.display()))?; + let (_, unknown) = + parse(&text).with_context(|| format!("invalid config file {}", path.display()))?; + match unknown.len() { + 0 => Ok(()), + 1 => bail!("unknown config key: {}", unknown[0]), + _ => bail!("unknown config keys: {}", unknown.join(", ")), + } +} + pub fn expand_home(path: &str, home: &Path) -> PathBuf { if path == "~" || path == "$HOME" { return home.to_path_buf(); @@ -294,6 +308,48 @@ mod tests { ); } + #[test] + fn check_accepts_valid_files() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write( + &path, + "[preview]\ntimeout_secs = 3\n[sound]\nvolume = 0.4\n", + ) + .unwrap(); + check(&path).unwrap(); + std::fs::write(&path, "").unwrap(); + check(&path).unwrap(); + } + + #[test] + fn check_rejects_invalid_values_with_valws_message() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[sound]\nvolume = 2.0\n").unwrap(); + let err = format!("{:#}", check(&path).unwrap_err()); + assert!( + err.contains("sound.volume must be between 0 and 1"), + "{err}" + ); + } + + #[test] + fn check_rejects_unknown_keys_by_name() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + std::fs::write(&path, "[preview]\ntimout_secs = 3\n").unwrap(); + assert_eq!( + check(&path).unwrap_err().to_string(), + "unknown config key: preview.timout_secs" + ); + std::fs::write(&path, "[preview]\ntimout_secs = 3\n[sund]\nx = 1\n").unwrap(); + assert_eq!( + check(&path).unwrap_err().to_string(), + "unknown config keys: preview.timout_secs, sund" + ); + } + #[test] fn missing_file_gives_defaults() { let dir = tempfile::tempdir().unwrap(); diff --git a/src/main.rs b/src/main.rs index e297c67..411c741 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,9 @@ enum Command { /// Internal: the process that shows preview thumbnails. #[command(name = "__preview-host", hide = true)] PreviewHost, + /// Internal: check a config file (home-manager runs it at build time). + #[command(name = "__check-config", hide = true)] + CheckConfig { file: PathBuf }, /// Internal: play the whole combo, for tuning the sounds by ear. #[command(name = "__combo-demo", hide = true)] ComboDemo, @@ -188,6 +191,7 @@ fn run(cli: Cli) -> Result<()> { Command::Window { common } => capture(Mode::Window, common), Command::Zoom { common } => capture(Mode::Zoom, common), Command::Edit { file } => editor::run(&file), + Command::CheckConfig { file } => config::check(&file), Command::ComboDemo => sound::demo(&config::load(&config::default_path())?.sound), Command::Toolbar => { let config = config::load(&config::default_path())?; @@ -429,6 +433,8 @@ mod tests { assert!(parses(&["__clipboard"])); assert!(parses(&["toolbar"])); assert!(parses(&["__combo-demo"])); + assert!(parses(&["__check-config", "c.toml"])); + assert!(!parses(&["__check-config"])); } #[test] @@ -530,6 +536,7 @@ mod tests { assert!(!help.contains("preview-host"), "{help}"); assert!(!help.contains("__clipboard"), "{help}"); assert!(!help.contains("combo-demo"), "{help}"); + assert!(!help.contains("check-config"), "{help}"); } #[test]