use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Role {
User,
Staff,
Supervisor,
Administrator,
Developer,
}
impl Role {
pub const fn rank(self) -> u32 {
match self {
Role::User => 100,
Role::Staff => 300,
Role::Supervisor => 600,
Role::Administrator => 900,
Role::Developer => 1000,
}
}
pub fn includes(self, other: Role) -> bool {
self.rank() >= other.rank()
}
pub fn as_str(self) -> &'static str {
match self {
Role::User => "user",
Role::Staff => "staff",
Role::Supervisor => "supervisor",
Role::Administrator => "administrator",
Role::Developer => "developer",
}
}
pub fn label(self) -> &'static str {
match self {
Role::User => "User",
Role::Staff => "Staff",
Role::Supervisor => "Supervisor",
Role::Administrator => "Administrator",
Role::Developer => "Developer",
}
}
pub fn parse(s: &str) -> Result<Self> {
match s {
"user" => Ok(Role::User),
"staff" => Ok(Role::Staff),
"supervisor" => Ok(Role::Supervisor),
"administrator" => Ok(Role::Administrator),
"developer" => Ok(Role::Developer),
other => Err(Error::BadRequest(format!("unknown role: {other}"))),
}
}
pub fn can_access_panel(self) -> bool {
self.rank() >= Role::Staff.rank()
}
pub fn bypasses_group_checks(self) -> bool {
matches!(self, Role::Administrator | Role::Developer)
}
}
pub const fn protected_roles() -> &'static [Role] {
&[Role::Administrator, Role::Developer]
}
impl serde::Serialize for Role {
fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
s.serialize_str(self.as_str())
}
}
impl<'de> serde::Deserialize<'de> for Role {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let s = <String as serde::Deserialize>::deserialize(d)?;
Role::parse(&s).map_err(serde::de::Error::custom)
}
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn includes_matrix_is_strict_ladder() {
let tiers = [
Role::User,
Role::Staff,
Role::Supervisor,
Role::Administrator,
Role::Developer,
];
for (i, &a) in tiers.iter().enumerate() {
for (j, &b) in tiers.iter().enumerate() {
assert_eq!(
a.includes(b),
i >= j,
"{a:?}.includes({b:?}) should be {}",
i >= j
);
}
}
}
#[test]
fn parse_round_trips_for_every_variant() {
for &r in &[
Role::User,
Role::Staff,
Role::Supervisor,
Role::Administrator,
Role::Developer,
] {
assert_eq!(Role::parse(r.as_str()).unwrap(), r);
}
}
#[test]
fn parse_rejects_unknown() {
assert!(Role::parse("admin").is_err());
assert!(Role::parse("root").is_err());
assert!(Role::parse("").is_err());
}
#[test]
fn can_access_panel_gates_at_staff() {
assert!(!Role::User.can_access_panel());
assert!(Role::Staff.can_access_panel());
assert!(Role::Supervisor.can_access_panel());
assert!(Role::Administrator.can_access_panel());
assert!(Role::Developer.can_access_panel());
}
#[test]
fn bypasses_group_checks_only_admin_and_dev() {
assert!(!Role::User.bypasses_group_checks());
assert!(!Role::Staff.bypasses_group_checks());
assert!(!Role::Supervisor.bypasses_group_checks());
assert!(Role::Administrator.bypasses_group_checks());
assert!(Role::Developer.bypasses_group_checks());
}
#[test]
fn label_is_capitalized_human_form() {
assert_eq!(Role::Administrator.label(), "Administrator");
assert_eq!(Role::Developer.label(), "Developer");
assert_eq!(Role::User.label(), "User");
}
}