use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MemoryType {
#[default]
Pattern,
Decision,
Fix,
Context,
}
impl MemoryType {
#[must_use]
pub fn section_name(&self) -> &'static str {
match self {
Self::Pattern => "Patterns",
Self::Decision => "Decisions",
Self::Fix => "Fixes",
Self::Context => "Context",
}
}
#[must_use]
pub fn from_section(s: &str) -> Option<Self> {
match s {
"Patterns" => Some(Self::Pattern),
"Decisions" => Some(Self::Decision),
"Fixes" => Some(Self::Fix),
"Context" => Some(Self::Context),
_ => None,
}
}
#[must_use]
pub fn emoji(&self) -> &'static str {
match self {
Self::Pattern => "🔄",
Self::Decision => "⚖️",
Self::Fix => "🔧",
Self::Context => "📍",
}
}
#[must_use]
pub fn all() -> &'static [Self] {
&[Self::Pattern, Self::Decision, Self::Fix, Self::Context]
}
}
impl std::fmt::Display for MemoryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pattern => write!(f, "pattern"),
Self::Decision => write!(f, "decision"),
Self::Fix => write!(f, "fix"),
Self::Context => write!(f, "context"),
}
}
}
impl std::str::FromStr for MemoryType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pattern" => Ok(Self::Pattern),
"decision" => Ok(Self::Decision),
"fix" => Ok(Self::Fix),
"context" => Ok(Self::Context),
_ => Err(format!(
"Invalid memory type: '{}'. Valid types: pattern, decision, fix, context",
s
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Memory {
pub id: String,
pub memory_type: MemoryType,
pub content: String,
pub tags: Vec<String>,
pub created: String,
}
impl Memory {
#[must_use]
pub fn new(memory_type: MemoryType, content: String, tags: Vec<String>) -> Self {
Self {
id: Self::generate_id(),
memory_type,
content,
tags,
created: chrono::Utc::now().format("%Y-%m-%d").to_string(),
}
}
#[must_use]
pub fn generate_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let timestamp = duration.as_secs();
let micros = duration.subsec_micros();
let hex_suffix = format!("{:04x}", micros % 0x10000);
format!("mem-{}-{}", timestamp, hex_suffix)
}
#[must_use]
pub fn matches_query(&self, query: &str) -> bool {
let query_lower = query.to_lowercase();
self.content.to_lowercase().contains(&query_lower)
|| self
.tags
.iter()
.any(|tag| tag.to_lowercase().contains(&query_lower))
}
#[must_use]
pub fn has_any_tag(&self, tags: &[String]) -> bool {
let tags_lower: Vec<String> = tags.iter().map(|t| t.to_lowercase()).collect();
self.tags
.iter()
.any(|t| tags_lower.contains(&t.to_lowercase()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_type_section_names() {
assert_eq!(MemoryType::Pattern.section_name(), "Patterns");
assert_eq!(MemoryType::Decision.section_name(), "Decisions");
assert_eq!(MemoryType::Fix.section_name(), "Fixes");
assert_eq!(MemoryType::Context.section_name(), "Context");
}
#[test]
fn test_memory_type_from_section() {
assert_eq!(
MemoryType::from_section("Patterns"),
Some(MemoryType::Pattern)
);
assert_eq!(
MemoryType::from_section("Decisions"),
Some(MemoryType::Decision)
);
assert_eq!(MemoryType::from_section("Fixes"), Some(MemoryType::Fix));
assert_eq!(
MemoryType::from_section("Context"),
Some(MemoryType::Context)
);
assert_eq!(MemoryType::from_section("Unknown"), None);
}
#[test]
fn test_memory_type_emojis() {
assert_eq!(MemoryType::Pattern.emoji(), "🔄");
assert_eq!(MemoryType::Decision.emoji(), "⚖️");
assert_eq!(MemoryType::Fix.emoji(), "🔧");
assert_eq!(MemoryType::Context.emoji(), "📍");
}
#[test]
fn test_memory_type_from_str() {
assert_eq!(
"pattern".parse::<MemoryType>().unwrap(),
MemoryType::Pattern
);
assert_eq!(
"DECISION".parse::<MemoryType>().unwrap(),
MemoryType::Decision
);
assert_eq!("Fix".parse::<MemoryType>().unwrap(), MemoryType::Fix);
assert_eq!(
"context".parse::<MemoryType>().unwrap(),
MemoryType::Context
);
assert!("invalid".parse::<MemoryType>().is_err());
}
#[test]
fn test_memory_type_display() {
assert_eq!(format!("{}", MemoryType::Pattern), "pattern");
assert_eq!(format!("{}", MemoryType::Decision), "decision");
assert_eq!(format!("{}", MemoryType::Fix), "fix");
assert_eq!(format!("{}", MemoryType::Context), "context");
}
#[test]
fn test_memory_new() {
let memory = Memory::new(
MemoryType::Pattern,
"Uses barrel exports".to_string(),
vec!["imports".to_string(), "structure".to_string()],
);
assert!(memory.id.starts_with("mem-"));
assert_eq!(memory.memory_type, MemoryType::Pattern);
assert_eq!(memory.content, "Uses barrel exports");
assert_eq!(memory.tags, vec!["imports", "structure"]);
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
assert_eq!(memory.created, today);
}
#[test]
fn test_memory_id_format() {
let id = Memory::generate_id();
assert!(id.starts_with("mem-"));
let parts: Vec<&str> = id.split('-').collect();
assert_eq!(parts.len(), 3);
assert_eq!(parts[0], "mem");
assert!(parts[1].parse::<u64>().is_ok()); assert_eq!(parts[2].len(), 4); }
#[test]
fn test_memory_matches_query() {
let memory = Memory {
id: "mem-123-abcd".to_string(),
memory_type: MemoryType::Pattern,
content: "Uses barrel exports for modules".to_string(),
tags: vec!["imports".to_string(), "structure".to_string()],
created: "2025-01-20".to_string(),
};
assert!(memory.matches_query("barrel"));
assert!(memory.matches_query("BARREL"));
assert!(memory.matches_query("imports"));
assert!(memory.matches_query("STRUCTURE"));
assert!(!memory.matches_query("authentication"));
}
#[test]
fn test_memory_has_any_tag() {
let memory = Memory {
id: "mem-123-abcd".to_string(),
memory_type: MemoryType::Fix,
content: "Docker fix".to_string(),
tags: vec!["docker".to_string(), "debugging".to_string()],
created: "2025-01-20".to_string(),
};
assert!(memory.has_any_tag(&["docker".to_string()]));
assert!(memory.has_any_tag(&["DEBUGGING".to_string()])); assert!(memory.has_any_tag(&["other".to_string(), "docker".to_string()]));
assert!(!memory.has_any_tag(&["unrelated".to_string()]));
}
#[test]
fn test_memory_type_all() {
let all = MemoryType::all();
assert_eq!(all.len(), 4);
assert_eq!(all[0], MemoryType::Pattern);
assert_eq!(all[1], MemoryType::Decision);
assert_eq!(all[2], MemoryType::Fix);
assert_eq!(all[3], MemoryType::Context);
}
#[test]
fn test_memory_type_default() {
assert_eq!(MemoryType::default(), MemoryType::Pattern);
}
#[test]
fn test_memory_serde_roundtrip() {
let memory = Memory {
id: "mem-123-abcd".to_string(),
memory_type: MemoryType::Decision,
content: "Chose Postgres".to_string(),
tags: vec!["database".to_string()],
created: "2025-01-20".to_string(),
};
let json = serde_json::to_string(&memory).unwrap();
let deserialized: Memory = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, memory.id);
assert_eq!(deserialized.memory_type, memory.memory_type);
assert_eq!(deserialized.content, memory.content);
assert_eq!(deserialized.tags, memory.tags);
assert_eq!(deserialized.created, memory.created);
}
#[test]
fn test_memory_type_serde() {
let mt = MemoryType::Decision;
let json = serde_json::to_string(&mt).unwrap();
assert_eq!(json, "\"decision\"");
let deserialized: MemoryType = serde_json::from_str("\"fix\"").unwrap();
assert_eq!(deserialized, MemoryType::Fix);
}
}