use std::{fmt, str::FromStr};
use oauth2_types::scope::ScopeToken as StrScopeToken;
pub use oauth2_types::scope::{InvalidScope, Scope};
use crate::PrivString;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ScopeToken {
Openid,
Profile,
Email,
Address,
Phone,
OfflineAccess,
MatrixApi(MatrixApiScopeToken),
MatrixDevice(PrivString),
Custom(PrivString),
}
impl ScopeToken {
pub fn try_with_matrix_device(device_id: String) -> Result<Self, InvalidScope> {
StrScopeToken::from_str(&device_id)?;
Ok(Self::MatrixDevice(PrivString(device_id)))
}
#[must_use]
pub fn matrix_device_id(&self) -> Option<&str> {
match &self {
Self::MatrixDevice(id) => Some(&id.0),
_ => None,
}
}
}
impl fmt::Display for ScopeToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ScopeToken::Openid => write!(f, "openid"),
ScopeToken::Profile => write!(f, "profile"),
ScopeToken::Email => write!(f, "email"),
ScopeToken::Address => write!(f, "address"),
ScopeToken::Phone => write!(f, "phone"),
ScopeToken::OfflineAccess => write!(f, "offline_access"),
ScopeToken::MatrixApi(scope) => {
write!(f, "urn:matrix:org.matrix.msc2967.client:api:{scope}")
}
ScopeToken::MatrixDevice(s) => {
write!(f, "urn:matrix:org.matrix.msc2967.client:device:{}", s.0)
}
ScopeToken::Custom(s) => f.write_str(&s.0),
}
}
}
impl From<StrScopeToken> for ScopeToken {
fn from(t: StrScopeToken) -> Self {
match &*t {
"openid" => Self::Openid,
"profile" => Self::Profile,
"email" => Self::Email,
"address" => Self::Address,
"phone" => Self::Phone,
"offline_access" => Self::OfflineAccess,
s => {
if let Some(matrix_scope) =
s.strip_prefix("urn:matrix:org.matrix.msc2967.client:api:")
{
Self::MatrixApi(
MatrixApiScopeToken::from_str(matrix_scope)
.expect("If the whole string is a valid scope, a substring is too"),
)
} else if let Some(device_id) =
s.strip_prefix("urn:matrix:org.matrix.msc2967.client:device:")
{
Self::MatrixDevice(PrivString(device_id.to_owned()))
} else {
Self::Custom(PrivString(s.to_owned()))
}
}
}
}
}
impl From<ScopeToken> for StrScopeToken {
fn from(t: ScopeToken) -> Self {
let s = t.to_string();
match StrScopeToken::from_str(&s) {
Ok(t) => t,
Err(_) => unreachable!(),
}
}
}
impl FromStr for ScopeToken {
type Err = InvalidScope;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let t = StrScopeToken::from_str(s)?;
Ok(t.into())
}
}
pub trait ScopeExt {
fn insert_token(&mut self, token: ScopeToken) -> bool;
fn contains_token(&self, token: &ScopeToken) -> bool;
}
impl ScopeExt for Scope {
fn insert_token(&mut self, token: ScopeToken) -> bool {
self.insert(token.into())
}
fn contains_token(&self, token: &ScopeToken) -> bool {
self.contains(&token.to_string())
}
}
impl FromIterator<ScopeToken> for Scope {
fn from_iter<T: IntoIterator<Item = ScopeToken>>(iter: T) -> Self {
iter.into_iter().map(Into::<StrScopeToken>::into).collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum MatrixApiScopeToken {
Full,
Guest,
Custom(PrivString),
}
impl fmt::Display for MatrixApiScopeToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Full => write!(f, "*"),
Self::Guest => write!(f, "guest"),
Self::Custom(s) => f.write_str(&s.0),
}
}
}
impl FromStr for MatrixApiScopeToken {
type Err = InvalidScope;
fn from_str(s: &str) -> Result<Self, Self::Err> {
StrScopeToken::from_str(s)?;
let t = match s {
"*" => Self::Full,
"guest" => Self::Guest,
_ => Self::Custom(PrivString(s.to_owned())),
};
Ok(t)
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
#[test]
fn parse_scope_token() {
assert_eq!(ScopeToken::from_str("openid"), Ok(ScopeToken::Openid));
let scope =
ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:device:ABCDEFGHIJKL")
.unwrap();
assert_matches!(scope, ScopeToken::MatrixDevice(_));
assert_eq!(scope.matrix_device_id(), Some("ABCDEFGHIJKL"));
let scope = ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:*").unwrap();
assert_eq!(scope, ScopeToken::MatrixApi(MatrixApiScopeToken::Full));
let scope = ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:guest").unwrap();
assert_eq!(scope, ScopeToken::MatrixApi(MatrixApiScopeToken::Guest));
let scope =
ScopeToken::from_str("urn:matrix:org.matrix.msc2967.client:api:my.custom.scope")
.unwrap();
let api_scope = assert_matches!(scope, ScopeToken::MatrixApi(s) => s);
assert_matches!(api_scope, MatrixApiScopeToken::Custom(_));
assert_eq!(api_scope.to_string(), "my.custom.scope");
assert_eq!(ScopeToken::from_str("invalid\\scope"), Err(InvalidScope));
assert_eq!(
MatrixApiScopeToken::from_str("invalid\\scope"),
Err(InvalidScope)
);
}
#[test]
fn display_scope_token() {
let scope = ScopeToken::MatrixApi(MatrixApiScopeToken::Full);
assert_eq!(
scope.to_string(),
"urn:matrix:org.matrix.msc2967.client:api:*"
);
let scope = ScopeToken::MatrixApi(MatrixApiScopeToken::Guest);
assert_eq!(
scope.to_string(),
"urn:matrix:org.matrix.msc2967.client:api:guest"
);
let api_scope = MatrixApiScopeToken::from_str("my.custom.scope").unwrap();
let scope = ScopeToken::MatrixApi(api_scope);
assert_eq!(
scope.to_string(),
"urn:matrix:org.matrix.msc2967.client:api:my.custom.scope"
);
}
#[test]
fn parse_scope() {
let scope = Scope::from_str("openid profile address").unwrap();
assert_eq!(scope.len(), 3);
assert!(scope.contains_token(&ScopeToken::Openid));
assert!(scope.contains_token(&ScopeToken::Profile));
assert!(scope.contains_token(&ScopeToken::Address));
assert!(!scope.contains_token(&ScopeToken::OfflineAccess));
}
#[test]
fn display_scope() {
let mut scope: Scope = [ScopeToken::Profile].into_iter().collect();
assert_eq!(scope.to_string(), "profile");
scope.insert_token(ScopeToken::MatrixApi(MatrixApiScopeToken::Full));
assert_eq!(
scope.to_string(),
"profile urn:matrix:org.matrix.msc2967.client:api:*"
);
}
}