Files
zesdex/crates/zesdex-cms/src/infrastructure/persistence/memory_repo.rs
T
asepharyana 1f0ae9f551 Refactor and clean up code across multiple modules
- Simplified token type assignment in OAuth service.
- Removed unused session_lock module and re-exported Session from zesdex_entities.
- Cleaned up session entity by removing unnecessary comments and code.
- Consolidated session handling in HTTP handlers for better readability.
- Improved formatting and readability in OAuth repository tests.
- Enhanced session lock repository with clearer match statements.
- Streamlined session repository error handling.
- Refined RNG tests for better clarity.
- Adjusted module visibility and organization in lib.rs.
- Updated IPC client and connection code for better error handling and clarity.
- Improved frame handling in IPC for better readability.
- Organized module imports and added test utilities for IPC.
- Enhanced database connection error handling.
- Simplified JWT token creation error handling.
- Improved password verification error handling.
- Cleaned up state management code for better readability.
- Refactored middleware for session authentication and rate limiting.
- Simplified clipboard utility for better error handling.
- Enhanced logging initialization for better error reporting.
- Improved pagination utility with clearer method annotations.
- Cleaned up sanitization functions for filenames and paths.
- Enhanced slug generation functions for better clarity and usability.
2026-07-17 09:08:41 +07:00

223 lines
7.8 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`.
//!
//! Each memory is stored as a `.md` file with YAML-ish frontmatter.
//! Filenames are derived from the memory's `name` via slugification.
//!
//! Frontmatter fields parsed from `---\n...\n---\n` header:
//! name, description, kind, created_at, updated_at, lifecycle,
//! outcome, scope, before, after, provenances
#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap
)]
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;
/// Persists `Memory` as markdown files with YAML-ish frontmatter.
#[derive(Debug, Clone, Default)]
pub struct MarkdownMemoryRepository;
impl MarkdownMemoryRepository {
/// Create a new repository instance.
pub fn new() -> Self {
Self
}
/// Build the frontmatter lines for a memory.
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`.
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 contents (frontmatter + body) into a `Memory`.
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 {
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)
}
fn load(&self, memory_dir: &Path, name: &str) -> Result<Memory> {
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(())
}
}