@jpgilldev / steid

steid/src/domain/membership.rs
2.7 KBRaw
1use std::{fmt, str::FromStr};
2
3use super::{DomainError, MembershipId, OrgId, UserId};
4
5/// What a member may do within an organisation.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub enum Role {
8 /// Full control: write access, settings, membership.
9 Owner,
10 /// Read access to private content, no write access.
11 Member,
12}
13
14impl Role {
15 pub fn as_str(self) -> &'static str {
16 match self {
17 Self::Owner => "owner",
18 Self::Member => "member",
19 }
20 }
21}
22
23impl fmt::Display for Role {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.write_str(self.as_str())
26 }
27}
28
29/// Parsing returns `Result`, never `Option`.
30///
31/// A silently-defaulted role is a bug that surfaces days later as the wrong
32/// permissions, so an unrecognised value must be impossible to ignore.
33impl FromStr for Role {
34 type Err = DomainError;
35
36 fn from_str(value: &str) -> Result<Self, Self::Err> {
37 match value {
38 "owner" => Ok(Self::Owner),
39 "member" => Ok(Self::Member),
40 other => Err(DomainError::validation(
41 "role",
42 format!("unknown role {other:?}"),
43 )),
44 }
45 }
46}
47
48/// Links a user to an organisation with a role.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct Membership {
51 pub id: MembershipId,
52 pub org_id: OrgId,
53 pub user_id: UserId,
54 pub role: Role,
55}
56
57impl Membership {
58 pub fn new(id: MembershipId, org_id: OrgId, user_id: UserId, role: Role) -> Self {
59 Self {
60 id,
61 org_id,
62 user_id,
63 role,
64 }
65 }
66
67 /// Whether this membership permits writes to the organisation.
68 pub fn can_write(&self) -> bool {
69 self.role == Role::Owner
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn roles_round_trip_through_strings() {
79 for role in [Role::Owner, Role::Member] {
80 assert_eq!(role.as_str().parse::<Role>().expect("round trip"), role);
81 }
82 }
83
84 #[test]
85 fn unknown_roles_are_an_error_not_a_default() {
86 let error = "admin".parse::<Role>().expect_err("should not parse");
87
88 assert!(matches!(error, DomainError::Validation { .. }));
89 }
90
91 #[test]
92 fn parsing_is_case_sensitive() {
93 assert!("Owner".parse::<Role>().is_err());
94 }
95
96 #[test]
97 fn only_owners_may_write() {
98 let membership = |role| {
99 Membership::new(
100 MembershipId::generate(),
101 OrgId::generate(),
102 UserId::generate(),
103 role,
104 )
105 };
106
107 assert!(membership(Role::Owner).can_write());
108 assert!(!membership(Role::Member).can_write());
109 }
110}