use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ScopeTier {
Low,
Medium,
High,
}
impl ScopeTier {
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Low => 0,
Self::Medium => 1,
Self::High => 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScopeEnforcer {
Pas,
Pcs,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Scope {
Openid,
Profile,
Email,
SessionVersion,
ChatRead,
ChatAck,
ChatSend,
ChatManage,
ContactRead,
ContactWrite,
ReactionWrite,
MyinfoRead,
MyinfoWrite,
MyprofileWrite,
AgentRead,
AgentWrite,
TemplateRead,
TemplateWrite,
ConsentManage,
StatsRead,
PlimsNumbersRead,
PlimsNumbersWrite,
PlimsAliasesRead,
PlimsAliasesWrite,
PlimsMasksRead,
PlimsMasksWrite,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScopeFacts {
pub scope: Scope,
pub wire: &'static str,
pub tier: ScopeTier,
pub enforcer: ScopeEnforcer,
}
pub const SCOPE_TABLE: [ScopeFacts; 26] = [
ScopeFacts {
scope: Scope::Openid,
wire: "openid",
tier: ScopeTier::Low,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::Profile,
wire: "profile",
tier: ScopeTier::Low,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::Email,
wire: "email",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::SessionVersion,
wire: "session_version",
tier: ScopeTier::Low,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::ChatRead,
wire: "chat.read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ChatAck,
wire: "chat.ack",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ChatSend,
wire: "chat.send",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ChatManage,
wire: "chat.manage",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ContactRead,
wire: "contact.read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ContactWrite,
wire: "contact.write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ReactionWrite,
wire: "reaction.write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::MyinfoRead,
wire: "myinfo.read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::MyinfoWrite,
wire: "myinfo.write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::MyprofileWrite,
wire: "myprofile.write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::AgentRead,
wire: "agent.read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::AgentWrite,
wire: "agent.write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::TemplateRead,
wire: "template.read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::TemplateWrite,
wire: "template.write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::ConsentManage,
wire: "consent.manage",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::StatsRead,
wire: "stats.read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pcs,
},
ScopeFacts {
scope: Scope::PlimsNumbersRead,
wire: "plims:numbers:read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::PlimsNumbersWrite,
wire: "plims:numbers:write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::PlimsAliasesRead,
wire: "plims:aliases:read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::PlimsAliasesWrite,
wire: "plims:aliases:write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::PlimsMasksRead,
wire: "plims:masks:read",
tier: ScopeTier::Medium,
enforcer: ScopeEnforcer::Pas,
},
ScopeFacts {
scope: Scope::PlimsMasksWrite,
wire: "plims:masks:write",
tier: ScopeTier::High,
enforcer: ScopeEnforcer::Pas,
},
];
impl Scope {
pub const ALL: [Scope; 26] = [
Self::Openid,
Self::Profile,
Self::Email,
Self::SessionVersion,
Self::ChatRead,
Self::ChatAck,
Self::ChatSend,
Self::ChatManage,
Self::ContactRead,
Self::ContactWrite,
Self::ReactionWrite,
Self::MyinfoRead,
Self::MyinfoWrite,
Self::MyprofileWrite,
Self::AgentRead,
Self::AgentWrite,
Self::TemplateRead,
Self::TemplateWrite,
Self::ConsentManage,
Self::StatsRead,
Self::PlimsNumbersRead,
Self::PlimsNumbersWrite,
Self::PlimsAliasesRead,
Self::PlimsAliasesWrite,
Self::PlimsMasksRead,
Self::PlimsMasksWrite,
];
pub const NAMES: [&'static str; 26] = {
let mut names = [""; 26];
let mut i = 0;
while i < SCOPE_TABLE.len() {
names[i] = SCOPE_TABLE[i].wire;
i += 1;
}
names
};
#[must_use]
pub const fn index(self) -> usize {
match self {
Self::Openid => 0,
Self::Profile => 1,
Self::Email => 2,
Self::SessionVersion => 3,
Self::ChatRead => 4,
Self::ChatAck => 5,
Self::ChatSend => 6,
Self::ChatManage => 7,
Self::ContactRead => 8,
Self::ContactWrite => 9,
Self::ReactionWrite => 10,
Self::MyinfoRead => 11,
Self::MyinfoWrite => 12,
Self::MyprofileWrite => 13,
Self::AgentRead => 14,
Self::AgentWrite => 15,
Self::TemplateRead => 16,
Self::TemplateWrite => 17,
Self::ConsentManage => 18,
Self::StatsRead => 19,
Self::PlimsNumbersRead => 20,
Self::PlimsNumbersWrite => 21,
Self::PlimsAliasesRead => 22,
Self::PlimsAliasesWrite => 23,
Self::PlimsMasksRead => 24,
Self::PlimsMasksWrite => 25,
}
}
#[must_use]
pub const fn facts(self) -> &'static ScopeFacts {
&SCOPE_TABLE[self.index()]
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.facts().wire
}
#[must_use]
pub const fn tier(self) -> ScopeTier {
self.facts().tier
}
#[must_use]
pub const fn enforcer(self) -> ScopeEnforcer {
self.facts().enforcer
}
#[must_use]
pub const fn is_plims(self) -> bool {
matches!(
self,
Self::PlimsNumbersRead
| Self::PlimsNumbersWrite
| Self::PlimsAliasesRead
| Self::PlimsAliasesWrite
| Self::PlimsMasksRead
| Self::PlimsMasksWrite
)
}
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|scope| scope.as_str() == s)
}
pub fn enforced_by(enforcer: ScopeEnforcer) -> impl Iterator<Item = Scope> {
Self::ALL
.into_iter()
.filter(move |scope| scope.enforcer() == enforcer)
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
const fn str_eq(a: &str, b: &str) -> bool {
let (a, b) = (a.as_bytes(), b.as_bytes());
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
const fn starts_with(s: &str, prefix: &str) -> bool {
let (s, p) = (s.as_bytes(), prefix.as_bytes());
if s.len() < p.len() {
return false;
}
let mut i = 0;
while i < p.len() {
if s[i] != p[i] {
return false;
}
i += 1;
}
true
}
const _: () = {
assert!(SCOPE_TABLE.len() == Scope::ALL.len());
assert!(Scope::NAMES.len() == Scope::ALL.len());
let mut i = 0;
while i < Scope::ALL.len() {
assert!(SCOPE_TABLE[i].scope.index() == Scope::ALL[i].index());
assert!(Scope::ALL[i].facts().scope.index() == Scope::ALL[i].index());
assert!(str_eq(Scope::NAMES[i], SCOPE_TABLE[i].wire));
let mut j = i + 1;
while j < Scope::ALL.len() {
assert!(
!str_eq(SCOPE_TABLE[i].wire, SCOPE_TABLE[j].wire),
"two scopes share a wire string"
);
j += 1;
}
let s = Scope::ALL[i];
assert!(
s.is_plims() == starts_with(s.as_str(), "plims:"),
"is_plims disagrees with the plims: namespace"
);
if s.is_plims() {
assert!(
matches!(s.enforcer(), ScopeEnforcer::Pas),
"a plims:* scope is gated on the :3103 face, which is PAS"
);
}
i += 1;
}
assert!(ScopeTier::Low.rank() < ScopeTier::Medium.rank());
assert!(ScopeTier::Medium.rank() < ScopeTier::High.rank());
assert!(Scope::ChatAck.tier().rank() < Scope::ChatSend.tier().rank());
assert!(Scope::ChatAck.tier().rank() < Scope::ChatManage.tier().rank());
assert!(Scope::ChatAck.tier().rank() < Scope::ReactionWrite.tier().rank());
};
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn parse_is_the_inverse_of_as_str() {
for s in Scope::ALL {
assert_eq!(Scope::parse(s.as_str()), Some(s));
}
assert_eq!(Scope::parse(""), None);
assert_eq!(Scope::parse("unknown"), None);
assert_eq!(Scope::parse("chat.readonly"), None);
assert_eq!(Scope::parse("CHAT.READ"), None);
}
#[test]
fn names_are_the_discovery_order() {
assert_eq!(Scope::NAMES[0], "openid");
assert_eq!(Scope::NAMES.last(), Some(&"plims:masks:write"));
let via_all: Vec<&str> = Scope::ALL.iter().map(|s| s.as_str()).collect();
assert_eq!(via_all, Scope::NAMES);
}
#[test]
fn pas_enforced_half_is_pinned() {
const EXPECTED: &[&str] = &[
"openid",
"profile",
"email",
"session_version",
"plims:numbers:read",
"plims:numbers:write",
"plims:aliases:read",
"plims:aliases:write",
"plims:masks:read",
"plims:masks:write",
];
let got: Vec<&str> = Scope::enforced_by(ScopeEnforcer::Pas)
.map(Scope::as_str)
.collect();
assert_eq!(
got, EXPECTED,
"the PAS-enforced half of the catalog drifted. If the new scope really \
is PAS's to enforce, add it here; if PCS matches it, mark it \
`ScopeEnforcer::Pcs` and key a PCS gate on it."
);
}
#[test]
fn both_halves_partition_the_vocabulary() {
let pas = Scope::enforced_by(ScopeEnforcer::Pas).count();
let pcs = Scope::enforced_by(ScopeEnforcer::Pcs).count();
assert!(pas > 0 && pcs > 0);
assert_eq!(pas + pcs, Scope::ALL.len());
}
#[test]
fn tiers_classify_as_documented() {
for s in [Scope::Openid, Scope::Profile, Scope::SessionVersion] {
assert_eq!(s.tier(), ScopeTier::Low, "{s}");
}
for s in [
Scope::Email,
Scope::ChatRead,
Scope::ChatAck,
Scope::ContactRead,
Scope::MyinfoRead,
Scope::AgentRead,
Scope::TemplateRead,
Scope::StatsRead,
Scope::PlimsNumbersRead,
Scope::PlimsAliasesRead,
Scope::PlimsMasksRead,
] {
assert_eq!(s.tier(), ScopeTier::Medium, "{s}");
}
for s in [
Scope::ChatSend,
Scope::ChatManage,
Scope::ContactWrite,
Scope::ReactionWrite,
Scope::MyinfoWrite,
Scope::AgentWrite,
Scope::TemplateWrite,
Scope::ConsentManage,
Scope::PlimsNumbersWrite,
Scope::PlimsAliasesWrite,
Scope::PlimsMasksWrite,
] {
assert_eq!(s.tier(), ScopeTier::High, "{s}");
}
assert!(ScopeTier::Low < ScopeTier::Medium && ScopeTier::Medium < ScopeTier::High);
}
#[cfg(feature = "serde")]
#[test]
fn tier_serde_is_the_variant_name() {
assert_eq!(
serde_json::to_string(&ScopeTier::Medium).unwrap(),
"\"Medium\""
);
assert_eq!(
serde_json::from_str::<ScopeTier>("\"High\"").unwrap(),
ScopeTier::High
);
}
}