use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GroupRole {
Owner,
Admin,
Moderator,
Member,
Guest,
}
impl GroupRole {
fn rank(self) -> u8 {
match self {
Self::Owner => 4,
Self::Admin => 3,
Self::Moderator => 2,
Self::Member => 1,
Self::Guest => 0,
}
}
#[must_use]
pub fn at_least(self, minimum: Self) -> bool {
self.rank() >= minimum.rank()
}
#[must_use]
pub fn outranks(self, other: Self) -> bool {
self.rank() > other.rank()
}
#[must_use]
pub fn rank_below(self, other: Self) -> bool {
self.rank() < other.rank()
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
match name.to_lowercase().as_str() {
"owner" => Some(Self::Owner),
"admin" => Some(Self::Admin),
"moderator" => Some(Self::Moderator),
"member" => Some(Self::Member),
"guest" => Some(Self::Guest),
_ => None,
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GroupMemberState {
#[default]
Active,
Pending,
Removed,
Banned,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GroupMember {
pub agent_id: String,
#[serde(default)]
pub user_id: Option<String>,
pub role: GroupRole,
pub state: GroupMemberState,
#[serde(default)]
pub display_name: Option<String>,
pub joined_at: u64,
pub updated_at: u64,
#[serde(default)]
pub added_by: Option<String>,
#[serde(default)]
pub removed_by: Option<String>,
#[serde(default)]
pub kem_public_key_b64: Option<String>,
}
impl GroupMember {
#[must_use]
pub fn new_owner(agent_id_hex: String, display_name: Option<String>, now_ms: u64) -> Self {
Self {
agent_id: agent_id_hex,
user_id: None,
role: GroupRole::Owner,
state: GroupMemberState::Active,
display_name,
joined_at: now_ms,
updated_at: now_ms,
added_by: None,
removed_by: None,
kem_public_key_b64: None,
}
}
#[must_use]
pub fn new_member(
agent_id_hex: String,
display_name: Option<String>,
added_by: Option<String>,
now_ms: u64,
) -> Self {
Self {
agent_id: agent_id_hex,
user_id: None,
role: GroupRole::Member,
state: GroupMemberState::Active,
display_name,
joined_at: now_ms,
updated_at: now_ms,
added_by,
removed_by: None,
kem_public_key_b64: None,
}
}
#[must_use]
pub fn is_active(&self) -> bool {
self.state == GroupMemberState::Active
}
#[must_use]
pub fn is_banned(&self) -> bool {
self.state == GroupMemberState::Banned
}
#[must_use]
pub fn is_removed(&self) -> bool {
self.state == GroupMemberState::Removed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn role_rank_ordering() {
assert!(GroupRole::Owner.outranks(GroupRole::Admin));
assert!(GroupRole::Admin.outranks(GroupRole::Moderator));
assert!(GroupRole::Moderator.outranks(GroupRole::Member));
assert!(GroupRole::Member.outranks(GroupRole::Guest));
}
#[test]
fn role_at_least() {
assert!(GroupRole::Owner.at_least(GroupRole::Admin));
assert!(GroupRole::Admin.at_least(GroupRole::Admin));
assert!(!GroupRole::Member.at_least(GroupRole::Admin));
}
#[test]
fn role_from_name() {
assert_eq!(GroupRole::from_name("admin"), Some(GroupRole::Admin));
assert_eq!(GroupRole::from_name("OWNER"), Some(GroupRole::Owner));
assert_eq!(GroupRole::from_name("bogus"), None);
}
#[test]
fn new_owner_is_active_owner() {
let m = GroupMember::new_owner("ff".repeat(32), None, 100);
assert_eq!(m.role, GroupRole::Owner);
assert!(m.is_active());
assert_eq!(m.joined_at, 100);
}
#[test]
fn new_member_is_plain_member() {
let m = GroupMember::new_member("aa".repeat(32), Some("Alice".into()), None, 200);
assert_eq!(m.role, GroupRole::Member);
assert!(m.is_active());
assert_eq!(m.display_name.as_deref(), Some("Alice"));
}
#[test]
fn banned_flag() {
let mut m = GroupMember::new_member("aa".repeat(32), None, None, 0);
m.state = GroupMemberState::Banned;
assert!(m.is_banned());
assert!(!m.is_active());
}
}