about summary refs log tree commit diff
path: root/kittybox-rs/indieauth/src/scopes.rs
diff options
context:
space:
mode:
Diffstat (limited to 'kittybox-rs/indieauth/src/scopes.rs')
-rw-r--r--kittybox-rs/indieauth/src/scopes.rs169
1 files changed, 169 insertions, 0 deletions
diff --git a/kittybox-rs/indieauth/src/scopes.rs b/kittybox-rs/indieauth/src/scopes.rs
new file mode 100644
index 0000000..6d1ed7e
--- /dev/null
+++ b/kittybox-rs/indieauth/src/scopes.rs
@@ -0,0 +1,169 @@
+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<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)
+        }
+    }
+}
+#[derive(Debug, Clone)]
+pub struct Scopes(Vec<Scope>);
+impl Scopes {
+    pub fn new(scopes: Vec<Scope>) -> 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| scopes.iter().any(|s2| s1 == s2))
+            .all(|s| s)
+    }
+}
+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 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(value.split_ascii_whitespace()
+                  .map(Scope::from)
+                  .collect::<Vec<Scope>>()))
+    }
+}
+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))
+        
+    }
+
+}