slave 0.9.0

CLI for slave, a Laravel-inspired Rust framework for MCP-first apps: scaffold an MCP backend with auth, roles and Toasty models and migrations, add server-rendered web apps, and route them by domain or path
//! UTC timestamps for migration file names, without a date-time dependency.

use std::time::{SystemTime, UNIX_EPOCH};

use anyhow::{Context, Result};

/// The current UTC time as a Toasty migration prefix, `YYYYMMDD_HHMMSS`
/// (the `Timestamp` prefix style of `toasty-cli`).
pub fn toasty_prefix_now() -> Result<String> {
    let stamp = format_utc(now_secs()?);
    Ok(format!("{}_{}", &stamp[..8], &stamp[8..]))
}

fn now_secs() -> Result<u64> {
    Ok(SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .context("system clock is set before 1970")?
        .as_secs())
}

fn format_utc(unix_secs: u64) -> String {
    let (days, rem) = (unix_secs / 86_400, unix_secs % 86_400);
    let (year, month, day) = civil_from_days(days);
    format!(
        "{year:04}{month:02}{day:02}{:02}{:02}{:02}",
        rem / 3600,
        rem % 3600 / 60,
        rem % 60
    )
}

/// Converts days since 1970-01-01 to a (year, month, day) date.
/// Howard Hinnant's `civil_from_days` algorithm, restricted to dates after the epoch.
fn civil_from_days(days: u64) -> (u64, u64, u64) {
    let z = days + 719_468;
    let era = z / 146_097;
    let doe = z % 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let day = doy - (153 * mp + 2) / 5 + 1;
    let month = if mp < 10 { mp + 3 } else { mp - 9 };
    let year = yoe + era * 400 + u64::from(month <= 2);
    (year, month, day)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn formats_known_instants() {
        assert_eq!(format_utc(0), "19700101000000");
        assert_eq!(format_utc(951_782_400), "20000229000000"); // leap day
        assert_eq!(format_utc(1_700_000_000), "20231114221320");
        assert_eq!(format_utc(1_791_741_599), "20261011175959");
    }

    #[test]
    fn toasty_prefix_has_date_and_time() {
        let prefix = toasty_prefix_now().unwrap();
        assert_eq!(prefix.len(), 15);
        assert_eq!(&prefix[8..9], "_");
        assert!(prefix.as_str() > "2026");
    }
}