moonlight-core 0.1.2

Shared comparison, diffing, classification, and JSONL storage primitives for Moonlight.
Documentation
use crate::ComparisonRun;
use std::{
    path::{Path, PathBuf},
    time::SystemTime,
};
use tokio::{
    fs,
    io::{AsyncBufReadExt, BufReader as TokioBufReader},
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct JsonlFileSignature {
    pub path: PathBuf,
    len: u64,
    modified: Option<SystemTime>,
}

pub(super) async fn load_runs_from_signature(
    signature: &[JsonlFileSignature],
) -> anyhow::Result<Vec<ComparisonRun>> {
    let mut runs = Vec::new();
    for file in signature {
        load_runs_from_file(&file.path, &mut runs).await?;
    }
    runs.sort_by_key(|run| run.timestamp);
    Ok(runs)
}

pub(super) async fn scan_jsonl_files(scan_dir: &Path) -> anyhow::Result<Vec<JsonlFileSignature>> {
    let mut signature = Vec::new();
    if !fs::try_exists(scan_dir).await? {
        return Ok(signature);
    }

    let mut entries = fs::read_dir(scan_dir).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        if path.extension().and_then(|value| value.to_str()) != Some("jsonl") {
            continue;
        }
        let metadata = entry.metadata().await?;
        signature.push(JsonlFileSignature {
            path,
            len: metadata.len(),
            modified: metadata.modified().ok(),
        });
    }

    signature.sort_by(|left, right| left.path.cmp(&right.path));
    Ok(signature)
}

pub(super) async fn load_runs_from_file(
    path: &Path,
    runs: &mut Vec<ComparisonRun>,
) -> anyhow::Result<()> {
    let file = fs::File::open(path).await?;
    let mut lines = TokioBufReader::new(file).lines();
    while let Some(line) = lines.next_line().await? {
        if line.trim().is_empty() {
            continue;
        }
        match serde_json::from_str::<ComparisonRun>(&line) {
            Ok(run) => runs.push(run),
            Err(error) => warn_corrupt_line(path, &error),
        }
    }
    Ok(())
}

pub(super) fn warn_corrupt_line(path: &Path, error: &serde_json::Error) {
    tracing::warn!(
        path = %path.display(),
        error = %error,
        "skipping corrupt moonlight JSONL run in {}: {error}",
        path.display()
    );
}