use async_trait::async_trait;
use chrono::{DateTime, Utc};
use std::collections::HashMap;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::errors::AppError;
pub fn generate_slug(name: &str) -> String {
name.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
#[derive(Debug, Clone)]
pub struct OrgEntity {
pub id: Uuid,
pub name: String,
pub slug: String,
pub logo_url: Option<String>,
pub is_personal: bool,
pub owner_id: Uuid,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl OrgEntity {
pub fn new(name: String, slug: String, owner_id: Uuid, is_personal: bool) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name,
slug,
logo_url: None,
is_personal,
owner_id,
created_at: now,
updated_at: now,
}
}
pub fn new_personal(user_id: Uuid, user_name: Option<&str>) -> Self {
let name = match user_name {
Some(n) if !n.is_empty() => format!("{}'s Workspace", n),
_ => "Personal Workspace".to_string(),
};
let slug = user_id.to_string();
Self::new(name, slug, user_id, true)
}
}
#[async_trait]
pub trait OrgRepository: Send + Sync {
async fn find_by_id(&self, id: Uuid) -> Result<Option<OrgEntity>, AppError>;
async fn find_by_ids(&self, ids: &[Uuid]) -> Result<Vec<OrgEntity>, AppError>;
async fn find_by_slug(&self, slug: &str) -> Result<Option<OrgEntity>, AppError>;
async fn find_by_user(&self, user_id: Uuid) -> Result<Vec<OrgEntity>, AppError>;
async fn create(&self, org: OrgEntity) -> Result<OrgEntity, AppError>;
async fn update(&self, org: OrgEntity) -> Result<OrgEntity, AppError>;
async fn delete(&self, id: Uuid) -> Result<(), AppError>;
async fn slug_exists(&self, slug: &str) -> Result<bool, AppError>;
async fn list_all(&self, limit: u32, offset: u32) -> Result<Vec<OrgEntity>, AppError>;
async fn count(&self) -> Result<u64, AppError>;
}
pub struct InMemoryOrgRepository {
orgs: RwLock<HashMap<Uuid, OrgEntity>>,
}
impl InMemoryOrgRepository {
pub fn new() -> Self {
Self {
orgs: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryOrgRepository {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl OrgRepository for InMemoryOrgRepository {
async fn find_by_id(&self, id: Uuid) -> Result<Option<OrgEntity>, AppError> {
let orgs = self.orgs.read().await;
Ok(orgs.get(&id).cloned())
}
async fn find_by_ids(&self, ids: &[Uuid]) -> Result<Vec<OrgEntity>, AppError> {
let orgs = self.orgs.read().await;
Ok(ids.iter().filter_map(|id| orgs.get(id).cloned()).collect())
}
async fn find_by_slug(&self, slug: &str) -> Result<Option<OrgEntity>, AppError> {
let orgs = self.orgs.read().await;
Ok(orgs.values().find(|o| o.slug == slug).cloned())
}
async fn find_by_user(&self, _user_id: Uuid) -> Result<Vec<OrgEntity>, AppError> {
tracing::warn!(
"InMemoryOrgRepository::find_by_user called - this always returns empty. \
Use membership_repo + find_by_ids pattern instead."
);
Ok(vec![])
}
async fn create(&self, org: OrgEntity) -> Result<OrgEntity, AppError> {
let mut orgs = self.orgs.write().await;
if orgs.values().any(|o| o.slug == org.slug) {
return Err(AppError::Validation(
"Organization slug already exists".into(),
));
}
orgs.insert(org.id, org.clone());
Ok(org)
}
async fn update(&self, org: OrgEntity) -> Result<OrgEntity, AppError> {
let mut orgs = self.orgs.write().await;
if !orgs.contains_key(&org.id) {
return Err(AppError::NotFound("Organization not found".into()));
}
if orgs.values().any(|o| o.slug == org.slug && o.id != org.id) {
return Err(AppError::Validation(
"Organization slug already exists".into(),
));
}
let mut updated = org;
updated.updated_at = Utc::now();
orgs.insert(updated.id, updated.clone());
Ok(updated)
}
async fn delete(&self, id: Uuid) -> Result<(), AppError> {
let mut orgs = self.orgs.write().await;
if orgs.remove(&id).is_none() {
return Err(AppError::NotFound("Organization not found".into()));
}
Ok(())
}
async fn slug_exists(&self, slug: &str) -> Result<bool, AppError> {
let orgs = self.orgs.read().await;
Ok(orgs.values().any(|o| o.slug == slug))
}
async fn list_all(&self, limit: u32, offset: u32) -> Result<Vec<OrgEntity>, AppError> {
let orgs = self.orgs.read().await;
let mut all_orgs: Vec<_> = orgs.values().cloned().collect();
all_orgs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(all_orgs
.into_iter()
.skip(offset as usize)
.take(limit as usize)
.collect())
}
async fn count(&self) -> Result<u64, AppError> {
let orgs = self.orgs.read().await;
Ok(orgs.len() as u64)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_org() {
let repo = InMemoryOrgRepository::new();
let owner_id = Uuid::new_v4();
let org = OrgEntity::new("Test Org".into(), "test-org".into(), owner_id, false);
let created = repo.create(org).await.unwrap();
assert_eq!(created.name, "Test Org");
assert_eq!(created.slug, "test-org");
assert!(!created.is_personal);
}
#[tokio::test]
async fn test_create_personal_org() {
let repo = InMemoryOrgRepository::new();
let user_id = Uuid::new_v4();
let org = OrgEntity::new_personal(user_id, Some("John"));
let created = repo.create(org).await.unwrap();
assert_eq!(created.name, "John's Workspace");
assert!(created.is_personal);
assert_eq!(created.owner_id, user_id);
}
#[tokio::test]
async fn test_find_by_slug() {
let repo = InMemoryOrgRepository::new();
let owner_id = Uuid::new_v4();
let org = OrgEntity::new("Test Org".into(), "test-org".into(), owner_id, false);
repo.create(org).await.unwrap();
let found = repo.find_by_slug("test-org").await.unwrap();
assert!(found.is_some());
assert_eq!(found.unwrap().name, "Test Org");
let not_found = repo.find_by_slug("nonexistent").await.unwrap();
assert!(not_found.is_none());
}
#[tokio::test]
async fn test_duplicate_slug_rejected() {
let repo = InMemoryOrgRepository::new();
let owner_id = Uuid::new_v4();
let org1 = OrgEntity::new("Org 1".into(), "same-slug".into(), owner_id, false);
let org2 = OrgEntity::new("Org 2".into(), "same-slug".into(), owner_id, false);
repo.create(org1).await.unwrap();
let result = repo.create(org2).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_update_org() {
let repo = InMemoryOrgRepository::new();
let owner_id = Uuid::new_v4();
let org = OrgEntity::new("Test Org".into(), "test-org".into(), owner_id, false);
let created = repo.create(org).await.unwrap();
let mut updated = created.clone();
updated.name = "Updated Org".into();
let result = repo.update(updated).await.unwrap();
assert_eq!(result.name, "Updated Org");
}
#[tokio::test]
async fn test_delete_org() {
let repo = InMemoryOrgRepository::new();
let owner_id = Uuid::new_v4();
let org = OrgEntity::new("Test Org".into(), "test-org".into(), owner_id, false);
let created = repo.create(org).await.unwrap();
repo.delete(created.id).await.unwrap();
let found = repo.find_by_id(created.id).await.unwrap();
assert!(found.is_none());
}
}