use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::hash::Hash;
#[cfg(any(test, feature = "serde"))]
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::Access;
use crate::traits::Conditions;
#[derive(Debug, Error, PartialEq)]
pub enum GroupMembershipError<ID> {
#[error("attempted to add a member who is already active in the group: {0}")]
AlreadyAdded(ID),
#[error("attempted to remove a member who is already inactive in the group: {0}")]
AlreadyRemoved(ID),
#[error("actor lacks sufficient access to update the group: {0}")]
InsufficientAccess(ID),
#[error("actor is not an active member of the group: {0}")]
InactiveActor(ID),
#[error("member is not an active member of the group: {0}")]
InactiveMember(ID),
#[error("actor is not known to the group: {0}")]
UnrecognisedActor(ID),
#[error("member is not known to the group: {0}")]
UnrecognisedMember(ID),
}
#[derive(Clone, Debug)]
#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
pub struct MemberState<C> {
pub(crate) member_counter: usize,
pub(crate) access: Access<C>,
pub(crate) access_counter: usize,
}
impl<C> MemberState<C>
where
C: Clone + Debug + PartialEq,
{
pub fn access(&self) -> Access<C> {
self.access.clone()
}
pub fn is_member(&self) -> bool {
!self.member_counter.is_multiple_of(2)
}
pub fn is_puller(&self) -> bool {
self.access.is_pull()
}
pub fn is_reader(&self) -> bool {
self.access.is_read()
}
pub fn is_writer(&self) -> bool {
self.access.is_write()
}
pub fn is_manager(&self) -> bool {
self.access.is_manage()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(any(test, feature = "serde"), derive(Deserialize, Serialize))]
pub struct GroupMembersState<ID, C>
where
ID: Hash + Eq,
{
pub(crate) members: HashMap<ID, MemberState<C>>,
}
impl<ID, C> Default for GroupMembersState<ID, C>
where
ID: Hash + Eq,
{
fn default() -> Self {
Self {
members: Default::default(),
}
}
}
impl<ID, C> GroupMembersState<ID, C>
where
ID: Clone + Hash + Eq,
C: Conditions,
{
pub fn members(&self) -> HashSet<ID> {
self.members
.iter()
.filter_map(|(id, state)| {
if state.is_member() {
Some(id.to_owned())
} else {
None
}
})
.collect::<HashSet<ID>>()
}
pub fn access_levels(&self) -> Vec<(ID, Access<C>)> {
self.members
.iter()
.filter_map(|(id, state)| {
if state.is_member() {
Some((id.to_owned(), state.access()))
} else {
None
}
})
.collect::<Vec<(ID, Access<C>)>>()
}
pub fn managers(&self) -> HashSet<ID> {
self.members
.iter()
.filter_map(|(id, state)| {
if state.is_member() && state.is_manager() {
Some(id.to_owned())
} else {
None
}
})
.collect::<HashSet<_>>()
}
}
pub fn create<ID: Clone + Eq + Hash, C: Conditions>(
initial_members: &[(ID, Access<C>)],
) -> GroupMembersState<ID, C> {
let mut members = HashMap::new();
for (id, access) in initial_members {
let member = MemberState {
member_counter: 1,
access: access.clone(),
access_counter: 0,
};
members.insert(id.clone(), member);
}
GroupMembersState { members }
}
pub fn add<ID: Clone + Eq + Hash, C: Conditions>(
state: GroupMembersState<ID, C>,
adder: ID,
added: ID,
access: Access<C>,
) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
let Some(adder_state) = state.members.get(&adder) else {
return Err(GroupMembershipError::UnrecognisedActor(adder));
};
if !adder_state.is_member() {
return Err(GroupMembershipError::InactiveActor(adder));
} else if !adder_state.is_manager() {
return Err(GroupMembershipError::InsufficientAccess(adder));
}
if let Some(added_state) = state.members.get(&added)
&& added_state.is_member()
{
return Err(GroupMembershipError::AlreadyAdded(added));
}
let mut state = state;
state
.members
.entry(added.clone())
.and_modify(|added| {
if !added.is_member() {
added.member_counter += 1;
added.access = access.clone();
added.access_counter = 0;
}
})
.or_insert(MemberState {
member_counter: 1,
access,
access_counter: 0,
});
Ok(state)
}
pub fn remove<ID: Eq + Hash, C: Conditions>(
state: GroupMembersState<ID, C>,
remover: ID,
removed: ID,
) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
let Some(remover_state) = state.members.get(&remover) else {
return Err(GroupMembershipError::UnrecognisedActor(remover));
};
if !remover_state.is_member() {
return Err(GroupMembershipError::InactiveActor(remover));
} else if !remover_state.is_manager() && remover != removed {
return Err(GroupMembershipError::InsufficientAccess(remover));
}
if !state.members.contains_key(&removed) {
return Err(GroupMembershipError::UnrecognisedMember(removed));
};
if let Some(removed_state) = state.members.get(&removed)
&& !removed_state.is_member()
{
return Err(GroupMembershipError::AlreadyRemoved(removed));
}
let mut state = state;
state.members.entry(removed).and_modify(|removed| {
if removed.is_member() {
removed.member_counter += 1;
removed.access_counter = 0;
}
});
Ok(state)
}
fn modify<ID: Eq + Hash, C: Conditions>(
state: GroupMembersState<ID, C>,
modifier: ID,
modified: ID,
access: Access<C>,
) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
let Some(modifier_state) = state.members.get(&modifier) else {
return Err(GroupMembershipError::UnrecognisedActor(modifier));
};
if !modifier_state.is_member() {
return Err(GroupMembershipError::InactiveActor(modifier));
} else if !modifier_state.is_manager() {
return Err(GroupMembershipError::InsufficientAccess(modifier));
}
if let Some(modified_state) = state.members.get(&modified) {
if !modified_state.is_member() {
return Err(GroupMembershipError::InactiveMember(modified));
}
} else {
return Err(GroupMembershipError::UnrecognisedMember(modified));
}
let mut state = state;
state.members.entry(modified).and_modify(|modified| {
if modified.access != access {
modified.access = access;
modified.access_counter += 1;
}
});
Ok(state)
}
pub fn promote<ID: Eq + Hash, C: Conditions>(
state: GroupMembersState<ID, C>,
promoter: ID,
promoted: ID,
access: Access<C>,
) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
if let Some(member) = state.members.get(&promoted) {
let new_state = if member.is_manager() {
state
} else {
modify(state, promoter, promoted, access)?
};
Ok(new_state)
} else {
Err(GroupMembershipError::UnrecognisedMember(promoted))
}
}
pub fn demote<ID: Eq + Hash, C: Conditions>(
state: GroupMembersState<ID, C>,
demoter: ID,
demoted: ID,
access: Access<C>,
) -> Result<GroupMembersState<ID, C>, GroupMembershipError<ID>> {
if let Some(member) = state.members.get(&demoted) {
let new_state = if member.is_puller() {
state
} else {
modify(state, demoter, demoted, access)?
};
Ok(new_state)
} else {
Err(GroupMembershipError::UnrecognisedMember(demoted))
}
}
pub fn merge<ID: Clone + Eq + Hash, C: Conditions>(
state_1: GroupMembersState<ID, C>,
state_2: GroupMembersState<ID, C>,
) -> GroupMembersState<ID, C> {
let mut next_state = state_2.clone();
for (id, member_state_1) in state_1.members {
if let Some(member_state) = next_state.members.get_mut(&id) {
if member_state_1.member_counter > member_state.member_counter {
member_state.member_counter = member_state_1.member_counter;
member_state.access = member_state_1.access.clone();
member_state.access_counter = member_state_1.access_counter;
}
if member_state_1.member_counter == member_state.member_counter {
if member_state_1.access_counter > member_state.access_counter {
member_state.access = member_state_1.access.clone();
member_state.access_counter = member_state_1.access_counter;
}
if member_state_1.access_counter == member_state.access_counter
&& member_state_1.access < member_state.access
{
member_state.access = member_state_1.access;
}
}
} else {
next_state.members.insert(id, member_state_1);
}
}
next_state
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_add_remove() {
let alice = 0;
let bob = 1;
let charlie = 2;
let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
let group_y = create(&initial_members);
assert!(group_y.members().contains(&alice));
assert!(group_y.members().contains(&bob));
assert!(group_y.managers().contains(&alice));
assert!(!group_y.managers().contains(&bob));
let group_y = add(group_y, alice, charlie, Access::write()).unwrap();
assert!(group_y.members().contains(&charlie));
let group_y = remove(group_y, alice, bob).unwrap();
assert!(!group_y.members().contains(&bob));
}
#[test]
fn self_remove() {
let alice = 0;
let bob = 1;
let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
let group_y = create(&initial_members);
assert!(group_y.members().contains(&alice));
assert!(group_y.members().contains(&bob));
assert!(group_y.managers().contains(&alice));
assert!(!group_y.managers().contains(&bob));
let group_y = remove(group_y, alice, alice).unwrap();
assert!(!group_y.members().contains(&alice));
assert!(group_y.members().contains(&bob));
}
#[test]
fn promote_demote_modify() {
let alice = 0;
let bob = 1;
let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
let group_y = create(&initial_members);
let group_y = promote(group_y, alice, bob, Access::write()).unwrap();
let group_y_clone = group_y.clone();
let bob_state = group_y_clone.members.get(&bob).unwrap();
assert!(bob_state.is_writer());
let group_y = demote(group_y.clone(), alice, bob, Access::read()).unwrap();
let group_y = modify(group_y, alice, bob, Access::manage()).unwrap();
let bob_state = group_y.members.get(&bob).unwrap();
assert!(bob_state.is_manager());
}
#[test]
fn add_errors() {
let alice = 0;
let bob = 1;
let charlie = 2;
let daphne = 3;
let initial_members = [(alice, <Access>::manage()), (bob, Access::read())];
let group_y = create(&initial_members);
let result = add(group_y.clone(), charlie, daphne, Access::read());
std::assert_matches!(result, Err(GroupMembershipError::UnrecognisedActor(_bob)));
let result = add(group_y.clone(), bob, daphne, Access::read());
std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
let result = add(group_y.clone(), alice, bob, Access::read());
std::assert_matches!(result, Err(GroupMembershipError::AlreadyAdded(_bob)));
let group_y = remove(group_y, alice, bob).unwrap();
let result = add(group_y, bob, daphne, Access::read());
std::assert_matches!(result, Err(GroupMembershipError::InactiveActor(_bob)));
}
#[test]
fn remove_errors() {
let alice = 0;
let bob = 1;
let charlie = 2;
let daphne = 3;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::read()),
];
let group_y = create(&initial_members);
let result = remove(group_y.clone(), daphne, charlie);
std::assert_matches!(
result,
Err(GroupMembershipError::UnrecognisedActor(_daphne))
);
let result = remove(group_y.clone(), bob, charlie);
std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
let result = remove(group_y.clone(), alice, daphne);
std::assert_matches!(
result,
Err(GroupMembershipError::UnrecognisedMember(_daphne))
);
let group_y = remove(group_y, alice, charlie).unwrap();
let result = remove(group_y, alice, charlie);
std::assert_matches!(result, Err(GroupMembershipError::AlreadyRemoved(_charlie)));
}
#[test]
fn promote_errors() {
let alice = 0;
let bob = 1;
let charlie = 2;
let daphne = 3;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::read()),
];
let group_y = create(&initial_members);
let result = promote(group_y.clone(), daphne, charlie, Access::manage());
std::assert_matches!(
result,
Err(GroupMembershipError::UnrecognisedActor(_daphne))
);
let result = promote(group_y.clone(), bob, charlie, Access::write());
std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
let result = promote(group_y.clone(), alice, daphne, Access::read());
std::assert_matches!(
result,
Err(GroupMembershipError::UnrecognisedMember(_daphne))
);
let group_y = remove(group_y, alice, charlie).unwrap();
let result = promote(group_y.clone(), alice, charlie, Access::pull());
std::assert_matches!(result, Err(GroupMembershipError::InactiveMember(_charlie)));
let result = promote(group_y, charlie, bob, Access::manage());
std::assert_matches!(result, Err(GroupMembershipError::InactiveActor(_charlie)));
}
#[test]
fn demote_errors() {
let alice = 0;
let bob = 1;
let charlie = 2;
let daphne = 3;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::read()),
];
let group_y = create(&initial_members);
let result = demote(group_y.clone(), daphne, charlie, Access::pull());
std::assert_matches!(
result,
Err(GroupMembershipError::UnrecognisedActor(_daphne))
);
let result = demote(group_y.clone(), bob, charlie, Access::pull());
std::assert_matches!(result, Err(GroupMembershipError::InsufficientAccess(_bob)));
let result = demote(group_y.clone(), alice, daphne, Access::read());
std::assert_matches!(
result,
Err(GroupMembershipError::UnrecognisedMember(_daphne))
);
let group_y = remove(group_y, alice, charlie).unwrap();
let result = demote(group_y.clone(), alice, charlie, Access::pull());
std::assert_matches!(result, Err(GroupMembershipError::InactiveMember(_charlie)));
let result = demote(group_y, charlie, bob, Access::pull());
std::assert_matches!(result, Err(GroupMembershipError::InactiveActor(_charlie)));
}
#[test]
fn merge_state_member() {
let alice = 0;
let bob = 1;
let charlie = 2;
let daphne = 3;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::pull()),
];
let group_y_i = create(&initial_members);
let group_y_ii = add(group_y_i.clone(), alice, daphne, Access::read()).unwrap();
let group_y = merge(group_y_i, group_y_ii);
assert!(group_y.members().contains(&daphne));
}
#[test]
fn merge_state_counter() {
let alice = 0;
let bob = 1;
let charlie = 2;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::pull()),
];
let group_y_i = create(&initial_members);
let group_y_ii = remove(group_y_i.clone(), alice, bob).unwrap();
let group_y_ii = add(group_y_ii, alice, bob, Access::read()).unwrap();
let group_y = merge(group_y_i, group_y_ii);
assert!(group_y.members().contains(&alice));
assert!(group_y.members().contains(&bob));
assert!(group_y.members().contains(&charlie));
let bob_state = group_y.members.get(&bob).unwrap();
assert!(bob_state.member_counter == 3);
}
#[test]
fn merge_state_access_counter() {
let alice = 0;
let bob = 1;
let charlie = 2;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::pull()),
];
let group_y_i = create(&initial_members);
let group_y_ii = promote(group_y_i.clone(), alice, charlie, Access::read()).unwrap();
let group_y_ii = demote(group_y_ii.clone(), alice, charlie, Access::pull()).unwrap();
let group_y = merge(group_y_i, group_y_ii);
let charlie_state = group_y.members.get(&charlie).unwrap();
assert!(charlie_state.access_counter == 2);
assert!(charlie_state.is_puller());
}
#[test]
fn merge_state_access() {
let alice = 0;
let bob = 1;
let charlie = 2;
let initial_members = [
(alice, <Access>::manage()),
(bob, Access::read()),
(charlie, Access::pull()),
];
let group_y = create(&initial_members);
let group_y_i = promote(group_y.clone(), alice, charlie, Access::read()).unwrap();
let group_y_i = demote(group_y_i.clone(), alice, charlie, Access::pull()).unwrap();
let group_y_ii = modify(group_y.clone(), alice, charlie, Access::manage()).unwrap();
let group_y_ii = demote(group_y_ii.clone(), alice, charlie, Access::read()).unwrap();
let group_y = merge(group_y_i.clone(), group_y_ii.clone());
let charlie_state = group_y.members.get(&charlie).unwrap();
assert!(charlie_state.is_puller());
}
}