aether-sessions 0.1.0

Persisted session logs and transcript reconstruction for Aether
Documentation
pub use crate::error::SessionLogError;
use crate::model::{SessionEvent, SessionMeta};
use serde_json::Error as JsonError;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

#[derive(Debug, Clone)]
pub struct SessionLine {
    pub line_number: usize,
    pub bytes_read: usize,
    pub raw: String,
}

#[derive(Debug)]
pub enum SessionLogEntry {
    Persisted { line: SessionLine, event: Box<SessionEvent> },
    Transient { line: SessionLine },
    Malformed { line: SessionLine, error: JsonError },
}

impl SessionLogEntry {
    pub fn parse(line: SessionLine) -> Self {
        match serde_json::from_str::<SessionEvent>(&line.raw) {
            Ok(event) if event.is_persisted() => Self::Persisted { line, event: Box::new(event) },
            Ok(_) => Self::Transient { line },
            Err(error) => Self::Malformed { line, error },
        }
    }

    pub fn line(&self) -> &SessionLine {
        match self {
            Self::Persisted { line, .. } | Self::Transient { line } | Self::Malformed { line, .. } => line,
        }
    }
}

pub struct SessionLog<T: BufRead> {
    reader: T,
    pub meta: SessionMeta,
    line_number: usize,
    bytes_read: usize,
    metadata_bytes: usize,
}

impl SessionLog<BufReader<File>> {
    pub fn open(path: impl AsRef<Path>) -> Result<Self, SessionLogError> {
        Self::from_reader(BufReader::new(File::open(path.as_ref())?))
    }
}

impl<T: BufRead> SessionLog<T> {
    pub fn from_reader(mut reader: T) -> Result<Self, SessionLogError> {
        let mut line = String::new();
        let mut line_number = 0;
        loop {
            line.clear();
            if reader.read_line(&mut line)? == 0 {
                return Err(SessionLogError::MissingMetadata);
            }
            line_number += 1;
            if !line.trim().is_empty() {
                break;
            }
        }
        let meta = serde_json::from_str(line.trim())
            .map_err(|source| SessionLogError::InvalidMetadata { line_number, source })?;
        Ok(Self { reader, meta, line_number, bytes_read: line.len(), metadata_bytes: line.len() })
    }

    pub fn bytes_read(&self) -> usize {
        self.bytes_read
    }

    pub fn meta_line_bytes(&self) -> usize {
        self.metadata_bytes
    }

    pub fn next_entry(&mut self) -> std::io::Result<Option<SessionLogEntry>> {
        let Some(line) = self.next_line()? else {
            return Ok(None);
        };
        let entry = SessionLogEntry::parse(line);
        Ok(Some(entry))
    }

    pub fn next_line(&mut self) -> std::io::Result<Option<SessionLine>> {
        let mut line = String::new();
        loop {
            line.clear();
            let bytes_read = self.reader.read_line(&mut line)?;
            if bytes_read == 0 {
                return Ok(None);
            }
            self.line_number += 1;
            self.bytes_read = self.bytes_read.saturating_add(bytes_read);
            let trimmed = line.trim();
            if !trimmed.is_empty() {
                return Ok(Some(SessionLine { line_number: self.line_number, bytes_read, raw: trimmed.to_string() }));
            }
        }
    }
}