use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::config::config_path;
const KEPT: usize = 50;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seen {
pub version: String,
#[serde(default)]
pub at: u64,
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Log {
#[serde(default)]
seen: Vec<Seen>,
}
pub fn record(version: &str) {
let Some(path) = log_path() else { return };
let mut log = read(&path);
if log.seen.last().is_some_and(|last| last.version == version) {
return;
}
log.seen.push(Seen {
version: version.to_owned(),
at: now_seconds(),
});
if log.seen.len() > KEPT {
log.seen.drain(..log.seen.len() - KEPT);
}
write(&path, &log);
}
pub fn seen() -> Vec<Seen> {
let Some(path) = log_path() else {
return Vec::new();
};
let mut history = read(&path).seen;
history.reverse();
history
}
fn read(path: &Path) -> Log {
std::fs::read_to_string(path)
.ok()
.and_then(|text| toml::from_str(&text).ok())
.unwrap_or_default()
}
fn write(path: &Path, log: &Log) -> Option<()> {
let directory = path.parent()?;
std::fs::create_dir_all(directory).ok()?;
std::fs::write(path, toml::to_string(log).ok()?).ok()
}
fn log_path() -> Option<PathBuf> {
Some(beside(&config_path().ok()?))
}
fn beside(config: &Path) -> PathBuf {
config.with_file_name("versions.toml")
}
fn now_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|since| since.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn log_of(versions: &[&str]) -> Log {
Log {
seen: versions
.iter()
.map(|version| Seen {
version: (*version).to_string(),
at: 1,
})
.collect(),
}
}
fn would_append(log: &Log, version: &str) -> bool {
log.seen.last().is_none_or(|last| last.version != version)
}
#[test]
fn it_should_note_a_version_it_has_never_seen() {
assert!(would_append(&log_of(&["0.17.0"]), "0.18.0"));
}
#[test]
fn it_should_not_note_the_version_it_is_already_on() {
assert!(!would_append(&log_of(&["0.17.0", "0.18.0"]), "0.18.0"));
}
#[test]
fn it_should_note_a_version_that_is_older_than_the_last_one() {
assert!(would_append(&log_of(&["0.18.0"]), "0.17.0"));
}
#[test]
fn it_should_keep_the_file_readable_by_a_person() {
let text = toml::to_string(&log_of(&["0.18.0"])).expect("the log serialises");
assert!(text.contains("version = \"0.18.0\""));
}
#[test]
fn it_should_read_back_what_it_wrote() {
let text = toml::to_string(&log_of(&["0.18.0"])).expect("the log serialises");
let read: Log = toml::from_str(&text).expect("what it wrote is what it reads");
assert_eq!(read.seen[0].version, "0.18.0");
}
#[test]
fn it_should_treat_a_corrupt_log_as_empty() {
assert!(toml::from_str::<Log>("this is not toml {{").is_err());
}
#[test]
fn it_should_keep_the_log_beside_the_profiles() {
assert_eq!(
beside(Path::new("/somewhere/fundaia/config.toml")),
PathBuf::from("/somewhere/fundaia/versions.toml"),
);
}
}