2026-07-19 17:05:27 +07:00
|
|
|
|
//! JSON file–backed `ConversationRepository` implementation.
|
2026-07-16 12:32:17 +07:00
|
|
|
|
//!
|
2026-07-19 17:05:27 +07:00
|
|
|
|
//! Stores `Conversation` as pretty-printed JSON at `<session_dir>/conversation.json`.
|
|
|
|
|
|
//! Uses atomic write (temp file + rename + fsync) for crash safety.
|
2026-07-16 12:32:17 +07:00
|
|
|
|
//!
|
2026-07-19 17:05:27 +07:00
|
|
|
|
//! ## Data Flow
|
|
|
|
|
|
//! - `load()`: read file → deserialize JSON → return Conversation
|
|
|
|
|
|
//! - `save()`: serialize Conversation → atomic write to conversation.json
|
2026-07-16 12:32:17 +07:00
|
|
|
|
|
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
2026-07-18 02:37:03 +07:00
|
|
|
|
use zesdex_utils::write_json_atomic;
|
2026-07-16 12:32:17 +07:00
|
|
|
|
|
|
|
|
|
|
use crate::domain::conversation::Conversation;
|
|
|
|
|
|
use crate::domain::repository::ConversationRepository;
|
|
|
|
|
|
|
2026-07-19 17:05:27 +07:00
|
|
|
|
/// File-based `ConversationRepository` that reads/writes `conversation.json`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Zero-allocation: the struct is a unit type marker.
|
2026-07-16 12:32:17 +07:00
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
|
|
|
|
pub struct JsonConversationRepository;
|
|
|
|
|
|
|
|
|
|
|
|
impl JsonConversationRepository {
|
|
|
|
|
|
/// Create a new repository instance.
|
|
|
|
|
|
pub fn new() -> Self {
|
|
|
|
|
|
Self
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl ConversationRepository for JsonConversationRepository {
|
2026-07-19 17:05:27 +07:00
|
|
|
|
/// Load a `Conversation` from `<session_dir>/conversation.json`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Flow: read file → parse JSON → return Conversation.
|
2026-07-16 12:32:17 +07:00
|
|
|
|
fn load(&self, session_dir: &Path) -> Result<Conversation> {
|
|
|
|
|
|
let path = session_dir.join("conversation.json");
|
|
|
|
|
|
let data = std::fs::read_to_string(&path)
|
|
|
|
|
|
.with_context(|| format!("failed to read conversation at '{}'", path.display()))?;
|
|
|
|
|
|
let conv: Conversation = serde_json::from_str(&data)
|
|
|
|
|
|
.with_context(|| format!("failed to parse conversation at '{}'", path.display()))?;
|
|
|
|
|
|
Ok(conv)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-19 17:05:27 +07:00
|
|
|
|
/// Persist a `Conversation` to `<session_dir>/conversation.json`.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Flow: create session dir → atomic JSON write → log success.
|
2026-07-16 12:32:17 +07:00
|
|
|
|
fn save(&self, session_dir: &Path, conversation: &Conversation) -> Result<()> {
|
2026-07-19 17:05:27 +07:00
|
|
|
|
tracing::debug!("saving conversation to {session_dir:?}");
|
2026-07-16 12:32:17 +07:00
|
|
|
|
std::fs::create_dir_all(session_dir)
|
|
|
|
|
|
.with_context(|| format!("failed to create session dir '{}'", session_dir.display()))?;
|
|
|
|
|
|
let path = session_dir.join("conversation.json");
|
2026-07-18 02:37:03 +07:00
|
|
|
|
write_json_atomic(&path, conversation, None)
|
|
|
|
|
|
.with_context(|| "failed to save conversation")?;
|
2026-07-16 12:32:17 +07:00
|
|
|
|
tracing::debug!("conversation saved to '{}'", path.display());
|
|
|
|
|
|
Ok(())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|