Files
zesdex/crates/zesdex-cms/src/infrastructure/persistence/memory_repo.rs
T
asepharyana 5aaedbf787 docs: tambah doc comment, logging, dan inline comments di semua 255 file
Meliputi:
- File-level //! doc comment: tujuan file, alur kerja, komponen utama
- Function-level /// doc comment: apa, parameter, return, flow, edge cases
- Struct/enum/trait /// doc comment: peran, field docs
- Tracing logging (tracing::info!/debug!/trace!/warn!/error!) di setiap fungsi
- Inline comments untuk variable dan branching logic penting
- Seluruh 8 crates di workspace: zesdex-backend, zesdex-cms, zesdex-entities,
  zesdex-iam, zesdex-infra, zesdex-ipc, zesdex-middleware, zesdex-utils
- Build: 0 errors, 242/242 tests passed
2026-07-19 17:05:47 +07:00

261 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Markdown filebacked `MemoryRepository` implementation.
//!
//! Each memory is stored as a `.md` file with YAML-ish frontmatter.
//! Filenames are derived from the memory's `name` via slugification
//! (see `Memory::slugify`).
//!
//! ## File Format
//! ```text
//! ---
//! name: my-memory
//! description: A useful lesson
//! kind: lesson
//! created_at: 1700000000
//! updated_at: 1700000000
//! lifecycle: active
//! outcome: success
//! scope: global
//! before: old content
//! after: new content
//! provenances: tool1, tool2
//! ---
//! Free-form markdown content body...
//! ```
//!
//! ## Data Flow
//! - `list()`: scan `*.md` files (excluding `MEMORY.md`), return slugs
//! - `load()`: read file → strip `---\n...\n---\n` frontmatter → parse fields
//! - `save()`: build frontmatter → write to temp file → rename atomically
//! - `delete()`: remove file from disk
//!
//! ## Atomicity
//! Writes use temp-file + rename + parent-directory fsync for crash safety.
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
use anyhow::{Context, Result};
use crate::domain::memory::Memory;
use crate::domain::repository::MemoryRepository;
/// File-based `MemoryRepository` that stores memories as `.md` files with frontmatter.
///
/// Each file has a YAML-ish `---\n...\n---\n` header followed by free-form
/// markdown content. Filenames are derived from `Memory.name` via slugification.
#[derive(Debug, Clone, Default)]
pub struct MarkdownMemoryRepository;
impl MarkdownMemoryRepository {
/// Create a new repository instance.
pub fn new() -> Self {
Self
}
/// Build the YAML-ish frontmatter string for a memory.
///
/// Only non-empty optional fields are included in the output.
fn build_frontmatter(memory: &Memory) -> String {
let outcome_line = memory
.outcome
.as_ref()
.map(|o| format!("outcome: {o}\n"))
.unwrap_or_default();
let scope_line = memory
.scope
.as_ref()
.map(|s| format!("scope: {s}\n"))
.unwrap_or_default();
let before_line = memory
.before_snippet
.as_ref()
.map(|s| format!("before: {s}\n"))
.unwrap_or_default();
let after_line = memory
.after_snippet
.as_ref()
.map(|s| format!("after: {s}\n"))
.unwrap_or_default();
let prov_line = if memory.provenances.is_empty() {
String::new()
} else {
format!("provenances: {}\n", memory.provenances.join(", "))
};
format!(
"name: {name}\ndescription: {desc}\nkind: {kind}\n\
created_at: {created}\nupdated_at: {updated}\nlifecycle: {lifecycle}\n\
{outcome}{scope}{before}{after}{prov}",
name = memory.name,
desc = memory.description,
kind = memory.kind,
created = memory.created_at,
updated = memory.updated_at,
lifecycle = memory.lifecycle,
outcome = outcome_line,
scope = scope_line,
before = before_line,
after = after_line,
prov = prov_line,
)
}
/// Parse frontmatter lines into a `HashMap<String, String>`.
///
/// Flow: split lines → for each non-empty line, split on first ':' → insert.
/// Malformed lines (no ':') are silently skipped.
fn parse_frontmatter(front: &str) -> HashMap<String, String> {
front
.lines()
.filter_map(|l| {
let mut it = l.splitn(2, ':');
Some((it.next()?.trim().to_string(), it.next()?.trim().to_string()))
})
.collect()
}
/// Parse a memory file's full contents (frontmatter + body) into a `Memory`.
///
/// Flow: strip `---\n` prefix → split on `\n---\n` → parse front half with
/// `parse_frontmatter()` → use back half as content body → build Memory.
///
/// Returns `InvalidData` error if the frontmatter delimiter is missing.
fn parse(content: &str) -> std::io::Result<Memory> {
let content = content.strip_prefix("---\n").unwrap_or(content);
let parts: Vec<&str> = content.splitn(2, "\n---\n").collect();
if parts.len() < 2 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"missing frontmatter",
));
}
let front = Self::parse_frontmatter(parts[0]);
let body = parts.get(1).unwrap_or(&"").trim().to_string();
Ok(Memory {
name: front.get("name").cloned().unwrap_or_default(),
description: front.get("description").cloned().unwrap_or_default(),
content: body,
kind: front
.get("kind")
.cloned()
.unwrap_or_else(|| "reference".to_string()),
created_at: front
.get("created_at")
.and_then(|v| v.parse().ok())
.unwrap_or(0),
updated_at: front
.get("updated_at")
.and_then(|v| v.parse().ok())
.unwrap_or(0),
outcome: front.get("outcome").cloned().filter(|s| !s.is_empty()),
lifecycle: front
.get("lifecycle")
.cloned()
.unwrap_or_else(|| "new".to_string()),
scope: front.get("scope").cloned().filter(|s| !s.is_empty()),
before_snippet: front.get("before").cloned().filter(|s| !s.is_empty()),
after_snippet: front.get("after").cloned().filter(|s| !s.is_empty()),
provenances: front
.get("provenances")
.cloned()
.map(|s| {
s.split(", ")
.map(std::string::ToString::to_string)
.collect()
})
.unwrap_or_default(),
})
}
}
impl MemoryRepository for MarkdownMemoryRepository {
/// List all memory slugs in `memory_dir` by scanning `*.md` files.
///
/// Flow: read directory entries → filter `*.md` → strip extension → exclude MEMORY.md.
/// Returns empty Vec if the directory doesn't exist.
fn list(&self, memory_dir: &Path) -> Result<Vec<String>> {
let Ok(entries) = std::fs::read_dir(memory_dir) else {
return Ok(Vec::new());
};
let slugs: Vec<String> = entries
.filter_map(std::result::Result::ok)
.filter(|e| e.path().extension().is_some_and(|x| x == "md"))
.filter_map(|e| {
let name = e.file_name().to_string_lossy().to_string();
// Skip special summary file
if name == "MEMORY.md" {
return None;
}
name.strip_suffix(".md")
.map(std::string::ToString::to_string)
})
.collect();
Ok(slugs)
}
/// Load a single `Memory` by name from `memory_dir`.
///
/// Flow: resolve file path → read file → parse frontmatter + body → return Memory.
fn load(&self, memory_dir: &Path, name: &str) -> Result<Memory> {
tracing::debug!("loading memory '{name}'");
let path = Memory::path(memory_dir, name);
let content = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read memory '{name}' at '{}'", path.display()))?;
let memory = Self::parse(&content)
.map_err(|e| anyhow::anyhow!("failed to parse memory '{name}': {e}"))?;
Ok(memory)
}
fn save(&self, memory_dir: &Path, memory: &Memory) -> Result<()> {
let path = Memory::path(memory_dir, &memory.name);
let parent = path.parent().unwrap();
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create memory dir '{}'", parent.display()))?;
let frontmatter = Self::build_frontmatter(memory);
let content = format!("---\n{frontmatter}---\n\n{}", memory.content);
let tmp = parent.join(format!(".{}.tmp", uuid::Uuid::new_v4()));
{
let mut f = std::fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&tmp)
.with_context(|| format!("failed to write temp file '{}'", tmp.display()))?;
f.write_all(content.as_bytes())?;
f.sync_all()?;
}
std::fs::rename(&tmp, &path).with_context(|| {
format!(
"failed to rename '{}' -> '{}'",
tmp.display(),
path.display()
)
})?;
if let Some(p) = path.parent() {
if let Ok(d) = std::fs::File::open(p) {
let _ = d.sync_all();
}
}
tracing::debug!("memory saved to '{}'", path.display());
Ok(())
}
fn delete(&self, memory_dir: &Path, name: &str) -> Result<()> {
let path = Memory::path(memory_dir, name);
if path.exists() {
std::fs::remove_file(&path).with_context(|| {
format!("failed to delete memory '{name}' at '{}'", path.display())
})?;
tracing::debug!("memory deleted: '{}'", path.display());
} else {
tracing::warn!(
"memory '{name}' not found at '{}', skipping delete",
path.display()
);
}
Ok(())
}
}