#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Scope {
Read = 0,
Write = 1,
Recall = 2,
Forget = 3,
Admin = 4,
TenantManagement = 5,
}
impl Scope {
pub const fn as_str(self) -> &'static str {
match self {
Scope::Read => "read",
Scope::Write => "write",
Scope::Recall => "recall",
Scope::Forget => "forget",
Scope::Admin => "admin",
Scope::TenantManagement => "tenant-management",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"read" => Some(Scope::Read),
"write" => Some(Scope::Write),
"recall" => Some(Scope::Recall),
"forget" => Some(Scope::Forget),
"admin" => Some(Scope::Admin),
"tenant-management" => Some(Scope::TenantManagement),
_ => None,
}
}
pub const fn all() -> [Scope; 6] {
[
Scope::Read,
Scope::Write,
Scope::Recall,
Scope::Forget,
Scope::Admin,
Scope::TenantManagement,
]
}
fn bit(self) -> u32 {
1u32 << (self as u8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ScopeSet(u32);
impl ScopeSet {
pub const fn empty() -> Self {
Self(0)
}
pub fn all() -> Self {
let mut s = Self::empty();
for sc in Scope::all() {
s.add(sc);
}
s
}
pub fn from_iter<I: IntoIterator<Item = Scope>>(iter: I) -> Self {
let mut s = Self::empty();
for sc in iter {
s.add(sc);
}
s
}
pub fn add(&mut self, scope: Scope) {
self.0 |= scope.bit();
}
pub fn remove(&mut self, scope: Scope) {
self.0 &= !scope.bit();
}
pub fn contains(&self, scope: Scope) -> bool {
self.0 & scope.bit() != 0
}
pub fn covers(&self, required: ScopeSet) -> bool {
(self.0 & required.0) == required.0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn iter(&self) -> impl Iterator<Item = Scope> + '_ {
let bits = self.0;
Scope::all()
.into_iter()
.filter(move |s| bits & s.bit() != 0)
}
pub fn to_csv(&self) -> String {
let parts: Vec<&'static str> = self.iter().map(Scope::as_str).collect();
parts.join(",")
}
pub fn parse_csv(s: &str) -> Result<Self, String> {
let mut set = Self::empty();
for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let sc = Scope::parse(part).ok_or_else(|| format!("unknown scope `{}`", part))?;
set.add(sc);
}
Ok(set)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scope_round_trip_via_string() {
for s in Scope::all() {
assert_eq!(Scope::parse(s.as_str()), Some(s));
}
}
#[test]
fn scope_parse_unknown_returns_none() {
assert_eq!(Scope::parse("nonsense"), None);
assert_eq!(Scope::parse(""), None);
assert_eq!(Scope::parse("READ"), None); }
#[test]
fn scope_string_form_pinned() {
assert_eq!(Scope::Read.as_str(), "read");
assert_eq!(Scope::Write.as_str(), "write");
assert_eq!(Scope::Recall.as_str(), "recall");
assert_eq!(Scope::Forget.as_str(), "forget");
assert_eq!(Scope::Admin.as_str(), "admin");
assert_eq!(Scope::TenantManagement.as_str(), "tenant-management");
}
#[test]
fn empty_set_contains_nothing() {
let s = ScopeSet::empty();
assert!(s.is_empty());
for sc in Scope::all() {
assert!(!s.contains(sc));
}
}
#[test]
fn add_and_remove_round_trip() {
let mut s = ScopeSet::empty();
s.add(Scope::Read);
s.add(Scope::Write);
assert!(s.contains(Scope::Read));
assert!(s.contains(Scope::Write));
assert!(!s.contains(Scope::Forget));
s.remove(Scope::Read);
assert!(!s.contains(Scope::Read));
assert!(s.contains(Scope::Write));
}
#[test]
fn all_set_contains_all_scopes() {
let s = ScopeSet::all();
for sc in Scope::all() {
assert!(s.contains(sc));
}
}
#[test]
fn covers_requires_every_required_scope() {
let granted = ScopeSet::from_iter([Scope::Read, Scope::Recall]);
assert!(granted.covers(ScopeSet::from_iter([Scope::Read])));
assert!(granted.covers(ScopeSet::from_iter([Scope::Read, Scope::Recall])));
assert!(!granted.covers(ScopeSet::from_iter([Scope::Read, Scope::Write])));
assert!(granted.covers(ScopeSet::empty())); }
#[test]
fn csv_round_trip() {
let s = ScopeSet::from_iter([Scope::Read, Scope::Forget, Scope::Admin]);
let csv = s.to_csv();
assert_eq!(csv, "read,forget,admin");
let parsed = ScopeSet::parse_csv(&csv).unwrap();
assert_eq!(parsed, s);
}
#[test]
fn parse_csv_handles_whitespace_and_empty_segments() {
let parsed = ScopeSet::parse_csv(" read , write ,, ").unwrap();
assert!(parsed.contains(Scope::Read));
assert!(parsed.contains(Scope::Write));
assert!(!parsed.contains(Scope::Admin));
}
#[test]
fn parse_csv_rejects_unknown_scope() {
let err = ScopeSet::parse_csv("read,nonsense").unwrap_err();
assert!(err.contains("nonsense"));
}
#[test]
fn iter_returns_in_stable_order() {
let s = ScopeSet::from_iter([Scope::Forget, Scope::Read, Scope::Admin]);
let collected: Vec<Scope> = s.iter().collect();
assert_eq!(collected, vec![Scope::Read, Scope::Forget, Scope::Admin]);
}
#[test]
fn bit_positions_are_distinct() {
let mut bits = std::collections::HashSet::new();
for sc in Scope::all() {
assert!(bits.insert(sc.bit()), "duplicate bit for {:?}", sc);
}
}
}