use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Ai,
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Role::User => write!(f, "user"),
Role::Ai => write!(f, "ai"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Category {
Intent,
Reasoning,
Error,
Note,
}
impl std::fmt::Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Category::Intent => write!(f, "intent"),
Category::Reasoning => write!(f, "reasoning"),
Category::Error => write!(f, "error"),
Category::Note => write!(f, "note"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Location {
pub file: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_line: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_line: Option<u32>,
}
impl Location {
pub fn file(path: impl Into<String>) -> Self {
Self {
file: path.into(),
start_line: None,
end_line: None,
}
}
pub fn line(path: impl Into<String>, line: u32) -> Self {
Self {
file: path.into(),
start_line: Some(line),
end_line: None,
}
}
pub fn range(path: impl Into<String>, start: u32, end: u32) -> Self {
Self {
file: path.into(),
start_line: Some(start),
end_line: Some(end),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexEntry {
pub role: Role,
pub category: Category,
pub content: String,
#[serde(with = "chrono::serde::ts_seconds")]
pub timestamp: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<Location>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line_number: Option<u32>,
}
impl IndexEntry {
pub fn new(role: Role, category: Category, content: impl Into<String>) -> Self {
Self {
role,
category,
content: content.into(),
timestamp: Utc::now(),
locations: None,
file_path: None,
line_number: None,
}
}
pub fn with_location(
role: Role,
category: Category,
content: impl Into<String>,
file_path: Option<String>,
line_number: Option<u32>,
) -> Self {
Self {
role,
category,
content: content.into(),
timestamp: Utc::now(),
locations: None,
file_path,
line_number,
}
}
pub fn with_locations(
role: Role,
category: Category,
content: impl Into<String>,
locations: Vec<Location>,
) -> Self {
Self {
role,
category,
content: content.into(),
timestamp: Utc::now(),
locations: if locations.is_empty() {
None
} else {
Some(locations)
},
file_path: None,
line_number: None,
}
}
pub fn get_locations(&self) -> Vec<Location> {
if let Some(ref locs) = self.locations {
return locs.clone();
}
if let Some(ref path) = self.file_path {
vec![Location {
file: path.clone(),
start_line: self.line_number,
end_line: None,
}]
} else {
vec![]
}
}
pub fn user_intent(content: impl Into<String>) -> Self {
Self::new(Role::User, Category::Intent, content)
}
pub fn ai_reasoning(content: impl Into<String>) -> Self {
Self::new(Role::Ai, Category::Reasoning, content)
}
pub fn user_note(content: impl Into<String>) -> Self {
Self::new(Role::User, Category::Note, content)
}
pub fn error(role: Role, content: impl Into<String>) -> Self {
Self::new(role, Category::Error, content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_index_entry_serialization() {
let entry = IndexEntry::user_intent("Fix the auth bug");
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("\"role\":\"user\""));
assert!(json.contains("\"category\":\"intent\""));
assert!(json.contains("Fix the auth bug"));
}
#[test]
fn test_index_entry_deserialization() {
let json = r#"{"role":"ai","category":"reasoning","content":"Plan: Use try/catch","timestamp":1704812345}"#;
let entry: IndexEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.role, Role::Ai);
assert_eq!(entry.category, Category::Reasoning);
assert_eq!(entry.content, "Plan: Use try/catch");
}
#[test]
fn test_role_display() {
assert_eq!(Role::User.to_string(), "user");
assert_eq!(Role::Ai.to_string(), "ai");
}
#[test]
fn test_category_display() {
assert_eq!(Category::Intent.to_string(), "intent");
assert_eq!(Category::Reasoning.to_string(), "reasoning");
}
}