rrq 0.11.1

RRQ orchestrator CLI and worker runtime.
Documentation
use chrono::{DateTime, Local, TimeZone, Utc};
use serde_json::Value;

pub fn parse_timestamp(raw: &str) -> Option<f64> {
    if raw.is_empty() {
        return None;
    }
    if let Ok(value) = raw.parse::<f64>() {
        return Some(value);
    }
    DateTime::parse_from_rfc3339(raw)
        .ok()
        .map(|dt| dt.timestamp_millis() as f64 / 1000.0)
}

pub fn format_timestamp(raw: Option<&str>) -> String {
    let Some(value) = raw.and_then(parse_timestamp) else {
        return "N/A".to_string();
    };
    let dt = Local.timestamp_opt(value as i64, 0).single();
    let Some(dt) = dt else {
        return value.to_string();
    };
    let now = Local::now();
    let diff = now.signed_duration_since(dt);
    let seconds = diff.num_seconds();
    if seconds < 60 {
        format!("{seconds}s ago")
    } else if seconds < 3600 {
        format!("{}m ago", seconds / 60)
    } else if seconds < 86_400 {
        format!("{}h ago", seconds / 3600)
    } else {
        dt.format("%Y-%m-%d %H:%M:%S").to_string()
    }
}

pub fn format_duration(seconds: Option<f64>) -> String {
    let Some(seconds) = seconds else {
        return "N/A".to_string();
    };
    if seconds < 0.001 {
        format!("{:.0}μs", seconds * 1_000_000.0)
    } else if seconds < 1.0 {
        format!("{:.1}ms", seconds * 1000.0)
    } else if seconds < 60.0 {
        format!("{seconds:.1}s")
    } else if seconds < 3600.0 {
        let minutes = (seconds / 60.0).floor() as i64;
        let secs = (seconds % 60.0).floor() as i64;
        format!("{minutes}m {secs}s")
    } else {
        let hours = (seconds / 3600.0).floor() as i64;
        let minutes = ((seconds % 3600.0) / 60.0).floor() as i64;
        format!("{hours}h {minutes}m")
    }
}

pub fn truncate(value: &str, max_len: usize) -> String {
    if value.len() <= max_len {
        return value.to_string();
    }
    let keep = max_len.saturating_sub(3);
    format!("{}...", &value[..keep])
}

pub fn parse_json(raw: Option<&str>) -> Option<Value> {
    let raw = raw?;
    if raw.eq_ignore_ascii_case("null") {
        return None;
    }
    serde_json::from_str(raw).ok()
}

pub fn format_status(raw: Option<&str>) -> String {
    raw.unwrap_or("UNKNOWN").to_uppercase()
}

pub fn to_utc_rfc3339(epoch_seconds: f64) -> String {
    let seconds = epoch_seconds.floor() as i64;
    let nanos = ((epoch_seconds - epoch_seconds.floor()) * 1_000_000_000.0) as u32;
    let dt = Utc.timestamp_opt(seconds, nanos).single();
    dt.map(|dt| dt.to_rfc3339())
        .unwrap_or_else(|| epoch_seconds.to_string())
}

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

    #[test]
    fn parse_timestamp_handles_numeric_and_rfc3339() {
        assert_eq!(parse_timestamp(""), None);
        assert_eq!(parse_timestamp("0"), Some(0.0));
        let parsed = parse_timestamp("2024-01-01T00:00:00Z");
        assert!(parsed.is_some());
    }

    #[test]
    fn format_timestamp_handles_missing() {
        assert_eq!(format_timestamp(None), "N/A");
        let formatted = format_timestamp(Some("0"));
        assert!(!formatted.is_empty());
        assert_ne!(formatted, "N/A");
    }

    #[test]
    fn format_duration_formats_ranges() {
        assert_eq!(format_duration(None), "N/A");
        assert_eq!(format_duration(Some(0.0000004)), "0μs");
        assert_eq!(format_duration(Some(0.0004)), "400μs");
        assert_eq!(format_duration(Some(0.5)), "500.0ms");
        assert_eq!(format_duration(Some(2.5)), "2.5s");
        assert_eq!(format_duration(Some(90.0)), "1m 30s");
    }

    #[test]
    fn truncate_and_status() {
        assert_eq!(truncate("short", 10), "short");
        assert_eq!(truncate("longer_than", 6), "lon...");
        assert_eq!(format_status(Some("pending")), "PENDING");
        assert_eq!(format_status(None), "UNKNOWN");
    }

    #[test]
    fn parse_json_and_rfc3339() {
        assert_eq!(parse_json(None), None);
        assert_eq!(parse_json(Some("null")), None);
        assert!(parse_json(Some("{\"a\":1}")).is_some());
        assert_eq!(to_utc_rfc3339(0.0), "1970-01-01T00:00:00+00:00");
    }
}