use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
pub const STATE_FILE_NAME: &str = "last_config.toml";
#[derive(Debug, Serialize, Deserialize)]
struct StoredConfigPath {
path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigSource {
Defaults,
Explicit(PathBuf),
Remembered(PathBuf),
}
pub fn resolve(
explicit: Option<PathBuf>,
remembered: Option<PathBuf>,
ignore_remembered: bool,
) -> ConfigSource {
if let Some(path) = explicit {
return ConfigSource::Explicit(path);
}
if ignore_remembered {
return ConfigSource::Defaults;
}
match remembered {
Some(path) => ConfigSource::Remembered(path),
None => ConfigSource::Defaults,
}
}
pub fn remembered(state_file: &Path) -> Option<PathBuf> {
let content = match std::fs::read_to_string(state_file) {
Ok(c) => c,
Err(e) => {
tracing::debug!(?state_file, "no remembered config: {e}");
return None;
}
};
match toml::from_str::<StoredConfigPath>(&content) {
Ok(stored) => Some(stored.path),
Err(e) => {
tracing::debug!(?state_file, "ignoring malformed remembered config: {e}");
None
}
}
}
pub fn remember(state_file: &Path, config_path: &Path) -> io::Result<()> {
let absolute = std::path::absolute(config_path).unwrap_or_else(|e| {
tracing::debug!(?config_path, "could not absolutize config path: {e}");
config_path.to_owned()
});
let stored = StoredConfigPath { path: absolute };
let content = toml::to_string_pretty(&stored)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
std::fs::write(state_file, content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_prefers_explicit_over_remembered() {
let source = resolve(
Some(PathBuf::from("/explicit.toml")),
Some(PathBuf::from("/remembered.toml")),
false,
);
assert_eq!(
source,
ConfigSource::Explicit(PathBuf::from("/explicit.toml"))
);
}
#[test]
fn resolve_uses_remembered_when_no_explicit() {
let source = resolve(None, Some(PathBuf::from("/remembered.toml")), false);
assert_eq!(
source,
ConfigSource::Remembered(PathBuf::from("/remembered.toml"))
);
}
#[test]
fn resolve_ignores_remembered_when_bypassed() {
let source = resolve(None, Some(PathBuf::from("/remembered.toml")), true);
assert_eq!(source, ConfigSource::Defaults);
}
#[test]
fn resolve_falls_back_to_defaults() {
assert_eq!(resolve(None, None, false), ConfigSource::Defaults);
}
#[test]
fn explicit_still_wins_when_remembered_is_bypassed() {
let source = resolve(Some(PathBuf::from("/explicit.toml")), None, true);
assert_eq!(
source,
ConfigSource::Explicit(PathBuf::from("/explicit.toml"))
);
}
#[test]
fn remember_then_recall_round_trips() {
let dir = tempfile::tempdir().unwrap();
let state = dir.path().join(STATE_FILE_NAME);
let config = dir.path().join("trail.toml");
std::fs::write(&config, "").unwrap();
remember(&state, &config).unwrap();
assert_eq!(remembered(&state), Some(config));
}
#[test]
fn remember_stores_an_absolute_path() {
let dir = tempfile::tempdir().unwrap();
let state = dir.path().join(STATE_FILE_NAME);
remember(&state, Path::new("trail.toml")).unwrap();
let recalled = remembered(&state).expect("a path should be remembered");
assert!(
recalled.is_absolute(),
"a relative --config path must be stored absolute so it still \
resolves when Trail is next launched from another directory; got {recalled:?}"
);
assert!(recalled.ends_with("trail.toml"));
}
#[test]
fn recall_returns_none_when_state_file_is_absent() {
let dir = tempfile::tempdir().unwrap();
assert_eq!(remembered(&dir.path().join("does-not-exist.toml")), None);
}
#[test]
fn recall_returns_none_for_a_malformed_state_file() {
let dir = tempfile::tempdir().unwrap();
let state = dir.path().join(STATE_FILE_NAME);
std::fs::write(&state, "this is not valid toml {{{").unwrap();
assert_eq!(remembered(&state), None);
}
#[test]
fn recall_returns_none_when_key_is_missing() {
let dir = tempfile::tempdir().unwrap();
let state = dir.path().join(STATE_FILE_NAME);
std::fs::write(&state, "unrelated = \"value\"\n").unwrap();
assert_eq!(remembered(&state), None);
}
#[test]
fn remember_overwrites_a_previous_path() {
let dir = tempfile::tempdir().unwrap();
let state = dir.path().join(STATE_FILE_NAME);
let first = dir.path().join("first.toml");
let second = dir.path().join("second.toml");
remember(&state, &first).unwrap();
remember(&state, &second).unwrap();
assert_eq!(remembered(&state), Some(second));
}
}