use std::collections::HashMap;
use std::sync::Arc;
use crate::namespace::Namespace;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Project {
pub id: String,
pub name: String,
pub description: String,
pub system_prompt: Option<String>,
pub enabled_tools: Vec<String>,
pub metadata: HashMap<String, serde_json::Value>,
pub active: bool,
pub created_at: String,
pub updated_at: String,
}
impl Project {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
let now = chrono::Utc::now().to_rfc3339();
Self {
id: id.into(),
name: name.into(),
description: String::new(),
system_prompt: None,
enabled_tools: Vec::new(),
metadata: HashMap::new(),
active: true,
created_at: now.clone(),
updated_at: now,
}
}
pub fn namespace(&self) -> Namespace {
Namespace::parse(&format!("project/{}", self.id))
}
pub fn user_namespace(&self, user_id: &str) -> Namespace {
Namespace::parse(&format!("project/{}/user/{}", self.id, user_id))
}
}
#[async_trait::async_trait]
pub trait ProjectStore: Send + Sync {
async fn save(&self, project: &Project) -> Result<(), ProjectError>;
async fn load(&self, id: &str) -> Result<Option<Project>, ProjectError>;
async fn delete(&self, id: &str) -> Result<bool, ProjectError>;
async fn list(&self) -> Result<Vec<Project>, ProjectError>;
}
pub struct InMemoryProjectStore {
projects: tokio::sync::RwLock<HashMap<String, Project>>,
}
impl InMemoryProjectStore {
pub fn new() -> Self {
Self {
projects: tokio::sync::RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryProjectStore {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl ProjectStore for InMemoryProjectStore {
async fn save(&self, project: &Project) -> Result<(), ProjectError> {
self.projects
.write()
.await
.insert(project.id.clone(), project.clone());
Ok(())
}
async fn load(&self, id: &str) -> Result<Option<Project>, ProjectError> {
Ok(self.projects.read().await.get(id).cloned())
}
async fn delete(&self, id: &str) -> Result<bool, ProjectError> {
Ok(self.projects.write().await.remove(id).is_some())
}
async fn list(&self) -> Result<Vec<Project>, ProjectError> {
Ok(self.projects.read().await.values().cloned().collect())
}
}
pub struct ProjectManager {
store: Arc<dyn ProjectStore>,
}
impl ProjectManager {
pub fn new(store: Arc<dyn ProjectStore>) -> Self {
Self { store }
}
pub async fn create(
&self,
id: impl Into<String>,
name: impl Into<String>,
) -> Result<Project, ProjectError> {
let id = id.into();
if self.store.load(&id).await?.is_some() {
return Err(ProjectError::AlreadyExists(id));
}
let project = Project::new(id, name);
self.store.save(&project).await?;
Ok(project)
}
pub async fn get(&self, id: &str) -> Result<Option<Project>, ProjectError> {
self.store.load(id).await
}
pub async fn update(&self, project: &mut Project) -> Result<(), ProjectError> {
project.updated_at = chrono::Utc::now().to_rfc3339();
self.store.save(project).await
}
pub async fn delete(&self, id: &str) -> Result<bool, ProjectError> {
self.store.delete(id).await
}
pub async fn list(&self) -> Result<Vec<Project>, ProjectError> {
self.store.list().await
}
pub async fn list_active(&self) -> Result<Vec<Project>, ProjectError> {
let all = self.store.list().await?;
Ok(all.into_iter().filter(|p| p.active).collect())
}
}
#[derive(Debug, thiserror::Error)]
pub enum ProjectError {
#[error("project already exists: {0}")]
AlreadyExists(String),
#[error("project not found: {0}")]
NotFound(String),
#[error("storage error: {0}")]
Storage(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn project_creation() {
let project = Project::new("my-bot", "My Bot");
assert_eq!(project.id, "my-bot");
assert_eq!(project.name, "My Bot");
assert!(project.active);
assert!(project.enabled_tools.is_empty());
}
#[test]
fn project_namespace() {
let project = Project::new("proj-1", "Test");
assert_eq!(project.namespace().key(), "project/proj-1");
}
#[test]
fn project_user_namespace() {
let project = Project::new("proj-1", "Test");
let ns = project.user_namespace("user-42");
assert_eq!(ns.key(), "project/proj-1/user/user-42");
}
#[test]
fn project_serialization() {
let project = Project::new("test", "Test Project");
let json = serde_json::to_string(&project).unwrap();
let deser: Project = serde_json::from_str(&json).unwrap();
assert_eq!(deser.id, "test");
assert_eq!(deser.name, "Test Project");
}
#[tokio::test]
async fn in_memory_store_crud() {
let store = InMemoryProjectStore::new();
let project = Project::new("p1", "Project 1");
store.save(&project).await.unwrap();
let loaded = store.load("p1").await.unwrap().unwrap();
assert_eq!(loaded.name, "Project 1");
assert!(store.load("nonexistent").await.unwrap().is_none());
assert!(store.delete("p1").await.unwrap());
assert!(!store.delete("p1").await.unwrap());
}
#[tokio::test]
async fn in_memory_store_list() {
let store = InMemoryProjectStore::new();
store.save(&Project::new("a", "A")).await.unwrap();
store.save(&Project::new("b", "B")).await.unwrap();
let list = store.list().await.unwrap();
assert_eq!(list.len(), 2);
}
#[tokio::test]
async fn manager_create_and_get() {
let store = Arc::new(InMemoryProjectStore::new());
let manager = ProjectManager::new(store);
let project = manager.create("test", "Test").await.unwrap();
assert_eq!(project.id, "test");
let loaded = manager.get("test").await.unwrap().unwrap();
assert_eq!(loaded.name, "Test");
}
#[tokio::test]
async fn manager_duplicate_create_fails() {
let store = Arc::new(InMemoryProjectStore::new());
let manager = ProjectManager::new(store);
manager.create("dup", "First").await.unwrap();
let err = manager.create("dup", "Second").await.unwrap_err();
assert!(matches!(err, ProjectError::AlreadyExists(_)));
}
#[tokio::test]
async fn manager_update() {
let store = Arc::new(InMemoryProjectStore::new());
let manager = ProjectManager::new(store);
let mut project = manager.create("upd", "Original").await.unwrap();
let original_updated = project.updated_at.clone();
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
project.name = "Updated".into();
manager.update(&mut project).await.unwrap();
let loaded = manager.get("upd").await.unwrap().unwrap();
assert_eq!(loaded.name, "Updated");
assert_ne!(loaded.updated_at, original_updated);
}
#[tokio::test]
async fn manager_list_active() {
let store = Arc::new(InMemoryProjectStore::new());
let manager = ProjectManager::new(store);
manager.create("active", "Active").await.unwrap();
let mut inactive = manager.create("inactive", "Inactive").await.unwrap();
inactive.active = false;
manager.update(&mut inactive).await.unwrap();
let active = manager.list_active().await.unwrap();
assert_eq!(active.len(), 1);
assert_eq!(active[0].id, "active");
}
#[tokio::test]
async fn manager_delete() {
let store = Arc::new(InMemoryProjectStore::new());
let manager = ProjectManager::new(store);
manager.create("del", "Delete Me").await.unwrap();
assert!(manager.delete("del").await.unwrap());
assert!(manager.get("del").await.unwrap().is_none());
}
}