use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
#[serde(rename_all = "lowercase")]
pub enum OrgRole {
#[default]
Member,
Admin,
Owner,
}
impl OrgRole {
pub fn has(&self, required: OrgRole) -> bool {
*self >= required
}
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"member" => Some(OrgRole::Member),
"admin" => Some(OrgRole::Admin),
"owner" => Some(OrgRole::Owner),
_ => None,
}
}
}
impl std::fmt::Display for OrgRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OrgRole::Member => write!(f, "member"),
OrgRole::Admin => write!(f, "admin"),
OrgRole::Owner => write!(f, "owner"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrgMember {
pub user: String,
pub role: OrgRole,
pub added_at: u64,
pub added_by: String,
}
impl OrgMember {
pub fn new(user: String, role: OrgRole, added_by: String) -> Self {
Self {
user,
role,
added_at: Self::now(),
added_by,
}
}
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Organization {
pub id: u64,
pub name: String,
pub display_name: String,
pub description: Option<String>,
pub created_by: String,
pub members: Vec<OrgMember>,
pub teams: HashSet<u64>,
pub repos: HashSet<String>,
pub created_at: u64,
pub updated_at: u64,
}
impl Organization {
pub fn new(id: u64, name: String, display_name: String, created_by: String) -> Self {
let now = Self::now();
let founder = OrgMember {
user: created_by.clone(),
role: OrgRole::Owner,
added_at: now,
added_by: created_by.clone(),
};
Self {
id,
name,
display_name,
description: None,
created_by,
members: vec![founder],
teams: HashSet::new(),
repos: HashSet::new(),
created_at: now,
updated_at: now,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self.updated_at = Self::now();
self
}
pub fn get_member(&self, user: &str) -> Option<&OrgMember> {
self.members.iter().find(|m| m.user == user)
}
pub fn is_member(&self, user: &str) -> bool {
self.get_member(user).is_some()
}
pub fn has_role(&self, user: &str, required: OrgRole) -> bool {
self.get_member(user)
.map(|m| m.role.has(required))
.unwrap_or(false)
}
pub fn is_owner(&self, user: &str) -> bool {
self.has_role(user, OrgRole::Owner)
}
pub fn is_admin(&self, user: &str) -> bool {
self.has_role(user, OrgRole::Admin)
}
pub fn add_member(&mut self, member: OrgMember) -> bool {
if self.is_member(&member.user) {
return false;
}
self.members.push(member);
self.updated_at = Self::now();
true
}
pub fn remove_member(&mut self, user: &str) -> Result<bool, &'static str> {
if let Some(member) = self.get_member(user) {
if member.role == OrgRole::Owner {
let owner_count = self
.members
.iter()
.filter(|m| m.role == OrgRole::Owner)
.count();
if owner_count <= 1 {
return Err("cannot remove last owner");
}
}
}
let before = self.members.len();
self.members.retain(|m| m.user != user);
let removed = self.members.len() < before;
if removed {
self.updated_at = Self::now();
}
Ok(removed)
}
pub fn update_member_role(
&mut self,
user: &str,
new_role: OrgRole,
) -> Result<bool, &'static str> {
if let Some(member) = self.get_member(user) {
if member.role == OrgRole::Owner && new_role != OrgRole::Owner {
let owner_count = self
.members
.iter()
.filter(|m| m.role == OrgRole::Owner)
.count();
if owner_count <= 1 {
return Err("cannot demote last owner");
}
}
}
for member in &mut self.members {
if member.user == user {
member.role = new_role;
self.updated_at = Self::now();
return Ok(true);
}
}
Ok(false)
}
pub fn add_team(&mut self, team_id: u64) {
self.teams.insert(team_id);
self.updated_at = Self::now();
}
pub fn remove_team(&mut self, team_id: u64) -> bool {
let removed = self.teams.remove(&team_id);
if removed {
self.updated_at = Self::now();
}
removed
}
pub fn add_repo(&mut self, repo_key: String) {
self.repos.insert(repo_key);
self.updated_at = Self::now();
}
pub fn remove_repo(&mut self, repo_key: &str) -> bool {
let removed = self.repos.remove(repo_key);
if removed {
self.updated_at = Self::now();
}
removed
}
pub fn owner_count(&self) -> usize {
self.members
.iter()
.filter(|m| m.role == OrgRole::Owner)
.count()
}
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_org_role_ordering() {
assert!(OrgRole::Member < OrgRole::Admin);
assert!(OrgRole::Admin < OrgRole::Owner);
}
#[test]
fn test_org_creation() {
let org = Organization::new(1, "acme".into(), "Acme Corp".into(), "abc123".into());
assert_eq!(org.id, 1);
assert_eq!(org.name, "acme");
assert_eq!(org.display_name, "Acme Corp");
assert_eq!(org.members.len(), 1);
assert!(org.is_owner("abc123"));
}
#[test]
fn test_add_remove_member() {
let mut org = Organization::new(1, "acme".into(), "Acme Corp".into(), "owner".into());
let member = OrgMember::new("user1".into(), OrgRole::Member, "owner".into());
assert!(org.add_member(member));
assert!(org.is_member("user1"));
assert!(!org.is_admin("user1"));
let admin = OrgMember::new("admin1".into(), OrgRole::Admin, "owner".into());
assert!(org.add_member(admin));
assert!(org.is_admin("admin1"));
assert!(!org.is_owner("admin1"));
assert!(org.remove_member("user1").unwrap());
assert!(!org.is_member("user1"));
assert!(org.remove_member("owner").is_err());
}
#[test]
fn test_role_update() {
let mut org = Organization::new(1, "acme".into(), "Acme Corp".into(), "owner1".into());
let owner2 = OrgMember::new("owner2".into(), OrgRole::Owner, "owner1".into());
org.add_member(owner2);
assert!(org.update_member_role("owner1", OrgRole::Admin).unwrap());
assert!(org.is_admin("owner1"));
assert!(!org.is_owner("owner1"));
assert!(org.update_member_role("owner2", OrgRole::Member).is_err());
}
}