use std::str::FromStr;
use serde::{
Serialize, Serializer,
Deserialize,
de::{
Deserializer, Visitor,
Error as DeserializeError
}
};
/// Various scopes that can be requested through IndieAuth.
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
/// Allows to create posts using Micropub.
Create,
/// Allows to edit posts using Micropub.
Update,
/// Allows to delete posts using Micropub.
Delete,
/// Allows to upload blobs to the media endpoint.
Media,
/// Allows to read feeds via Microsub.
Read,
/// Allows to manage follows via Microsub.
Follow,
/// Allows to mute and unmute users in feeds via Microsub.
Mute,
/// Allows to block and unblock users.
Block,
/// Allows to create and manage feeds via Microsub.
Channels,
/// Allows to request profile information (except email, see Email)
Profile,
/// Allows to receive email in the profile information.
Email,
/// Custom scope not included above.
Custom(String)
}
impl Scope {
/// Create a custom scope from a string slice.
pub fn custom(scope: &str) -> Scope {
Scope::Custom(scope.to_string())
}
}
// TODO consider relying on serde_variant for these conversions
impl AsRef<str> for Scope {
fn as_ref(&self) -> &str {
use Scope::*;
match self {
Create => "create",
Update => "update",
Delete => "delete",
Media => "media",
Read => "read",
Follow => "follow",
Mute => "mute",
Block => "block",
Channels => "channels",
Profile => "profile",
Email => "email",
Custom(s) => s.as_ref()
}
}
}
impl From<&str> for Scope {
fn from(scope: &str) -> Self {
match scope {
"create" => Scope::Create,
"update" => Scope::Update,
"delete" => Scope::Delete,
"media" => Scope::Media,
"read" => Scope::Read,
"follow" => Scope::Follow,
"mute" => Scope::Mute,
"block" => Scope::Block,
"channels" => Scope::Channels,
"profile" => Scope::Profile,
"email" => Scope::Email,
other => Scope::custom(other)
}
}
}
impl FromStr for Scope {
type Err = std::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(s.into())
}
}
/// A list of scopes that serializes to a space-separated string instead of a list.
///
/// OAuth2 is weird, don't ask me why it's a thing.
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct Scopes(Vec<Scope>);
impl Scopes {
/// Create a list of scopes from a vector of scopes.
pub fn new(scopes: Vec<Scope>) -> Self {
Self(scopes)
}
/// Ensure a certain scope is listed in the scope list.
pub fn has(&self, scope: &Scope) -> bool {
self.0.iter().any(|s| s == scope)
}
/// Ensure all of the requested scopes are in the list.
pub fn has_all(&self, scopes: &[Scope]) -> bool {
scopes.iter()
.map(|s1| self.iter().any(|s2| s1 == s2))
.all(|s| s)
}
/// Transform this into an iterator over individual scopes.
pub fn iter(&self) -> std::slice::Iter<'_, Scope> {
self.0.iter()
}
}
impl AsRef<[Scope]> for Scopes {
fn as_ref(&self) -> &[Scope] {
self.0.as_ref()
}
}
// Explicit implementation of ToString because of specific requirements.
#[allow(clippy::to_string_trait_impl)]
impl ToString for Scopes {
fn to_string(&self) -> String {
self.0.iter()
.map(|s| s.as_ref())
.fold(String::new(), |a, s| if a.is_empty() {
s.to_string()
} else {
a + " " + s
})
}
}
impl FromStr for Scopes {
type Err = std::convert::Infallible;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Self(value.split_ascii_whitespace()
.map(Scope::from)
.collect::<Vec<Scope>>()))
}
}
impl Serialize for Scopes {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer
{
serializer.serialize_str(&self.to_string())
}
}
struct ScopeVisitor;
impl<'de> Visitor<'de> for ScopeVisitor {
type Value = Scopes;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string of space-separated OAuth2 scopes")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: DeserializeError
{
Ok(Scopes::from_str(value).unwrap())
}
}
impl<'de> Deserialize<'de> for Scopes {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>
{
deserializer.deserialize_str(ScopeVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serde_vec_scope() {
let scopes = vec![
Scope::Create, Scope::Update, Scope::Delete,
Scope::Media,
Scope::custom("kittybox_internal_access")
];
let scope_serialized = serde_json::to_value(
Scopes::new(scopes.clone())
).unwrap();
let scope_str = scope_serialized.as_str().unwrap();
assert_eq!(scope_str, "create update delete media kittybox_internal_access");
assert!(serde_json::from_value::<Scopes>(scope_serialized).unwrap().has_all(&scopes))
}
#[test]
fn test_scope_has_all() {
let scopes = Scopes(vec![
Scope::Create, Scope::Update, Scope::custom("draft")
]);
assert!(scopes.has_all(&[Scope::Create, Scope::custom("draft")]));
assert!(!scopes.has_all(&[Scope::Read, Scope::custom("kittybox_internal_access")]));
}
}