use std::str::FromStr; use serde::{ Serialize, Serializer, Deserialize, de::{ Deserializer, Visitor, Error as DeserializeError } }; #[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 { pub fn custom(scope: &str) -> Scope { Scope::Custom(scope.to_string()) } } // TODO consider relying on serde for these conversions impl AsRef 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) } } } #[derive(Debug, Clone)] pub struct Scopes(Vec); impl Scopes { pub fn new(scopes: Vec) -> Self { Self(scopes) } pub fn has(&self, scope: &Scope) -> bool { self.0.iter().any(|s| s == scope) } pub fn has_all(&self, scopes: &[Scope]) -> bool { scopes.iter() .map(|s1| self.iter().any(|s2| s1 == s2)) .all(|s| s) } 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() } } 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 { Ok(Self(value.split_ascii_whitespace() .map(Scope::from) .collect::>())) } } impl Serialize for Scopes { fn serialize(&self, serializer: S) -> Result 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(self, value: &str) -> Result where E: DeserializeError { Ok(Scopes::from_str(value).unwrap()) } } impl<'de> Deserialize<'de> for Scopes { fn deserialize(deserializer: D) -> Result 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::(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")])); } }