fundaia 0.52.3

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
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"),
        );
    }
}