#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ScopeInfo {
pub name: &'static str,
pub docs: &'static str,
}
impl ScopeInfo {
pub fn description(&self) -> String {
self.docs
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
}
pub trait Scopes: Copy + 'static {
const ALL: &'static [ScopeInfo];
}
pub fn is_scope_name(name: &str) -> bool {
!name.is_empty()
&& name
.bytes()
.all(|byte| matches!(byte, 0x21 | 0x23..=0x5b | 0x5d..=0x7e))
}
#[macro_export]
macro_rules! scopes {
(
$(#[$meta:meta])*
$vis:vis enum $name:ident {
$(
$(#[doc = $doc:literal])*
$variant:ident = $scope:literal
),+ $(,)?
}
) => {
$(#[$meta])*
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
$vis enum $name {
$(
$(#[doc = $doc])*
$variant,
)+
}
impl $name {
pub const fn as_str(self) -> &'static str {
match self {
$(Self::$variant => $scope,)+
}
}
}
impl $crate::Scopes for $name {
const ALL: &'static [$crate::ScopeInfo] = &[
$($crate::ScopeInfo {
name: $scope,
docs: ::core::concat!($($doc, "\n",)*),
},)+
];
}
};
}
#[cfg(test)]
mod tests {
use super::*;
crate::scopes! {
pub(crate) enum Scope {
PostsRead = "posts:read",
Undocumented = "misc",
}
}
#[test]
fn the_macro_names_and_describes_each_scope() {
assert_eq!(Scope::PostsRead.as_str(), "posts:read");
assert_eq!(Scope::Undocumented.as_str(), "misc");
assert_eq!(Scope::ALL.len(), 2);
assert_eq!(Scope::ALL[0].description(), "Read your posts and drafts.");
assert_eq!(Scope::ALL[1].description(), "");
}
#[test]
fn scope_names_follow_rfc_6749() {
for name in ["openid", "posts:read", "https://api.example/read", "a!#[]~"] {
assert!(is_scope_name(name), "{name}");
}
for name in ["", "two words", "quote\"", "back\\slash", "tab\t", "é"] {
assert!(!is_scope_name(name), "{name}");
}
}
}