relay-core 0.2.0-beta.4

The core components of the Relay Protocol.
Documentation
use serde::{
    Deserialize, Deserializer, Serialize, Serializer,
    de::{self, Visitor},
};

use super::{IdentityError as Err, InboxId, canonical_identity_string, is_valid_identity_string};
use crate::prelude::Address;

/// A unique identifier for a User within an Agent's domain.
/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#user-identity
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserId {
    inbox: Option<InboxId>,
    id: String,
}

impl From<Address> for UserId {
    fn from(address: Address) -> Self {
        address.user().clone()
    }
}

impl TryFrom<&str> for UserId {
    type Error = Err;

    fn try_from(value: &str) -> Result<Self, Err> {
        UserId::parse(value)
    }
}

impl TryFrom<String> for UserId {
    type Error = Err;

    fn try_from(value: String) -> Result<Self, Err> {
        UserId::parse(value)
    }
}

impl UserId {
    /// Parse a UserId from a string according to the identity rules.
    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#user-identity
    ///
    /// # Examples
    /// ```
    /// let user_id = relay_core::id::UserId::parse("Alice.Smith").unwrap();
    /// assert_eq!(user_id.canonical(), "#alice.smith");
    /// ```
    /// ```
    /// let user_id = relay_core::id::UserId::parse("Work#Alice.Smith").unwrap();
    /// assert_eq!(user_id.canonical(), "work#alice.smith");
    /// ```
    pub fn parse(input: impl AsRef<str>) -> Result<Self, Err> {
        let input = input.as_ref();

        if let Some((inbox_str, user_str)) = input.split_once('#')
            && !inbox_str.is_empty()
        {
            let inbox = InboxId::parse(inbox_str)?;
            let id = Self::parse_user_id(user_str)?;
            Ok(UserId {
                inbox: Some(inbox),
                id,
            })
        } else {
            let id = Self::parse_user_id(input.trim_start_matches("#"))?;
            Ok(UserId { inbox: None, id })
        }
    }

    fn parse_user_id(input: &str) -> Result<String, Err> {
        if input.is_empty() {
            return Err(Err::InvalidUser);
        }
        if !is_valid_identity_string(input) {
            return Err(Err::InvalidIdentityString);
        }

        Ok(canonical_identity_string(input))
    }

    /// Replace the current UserId with a new value.
    pub fn replace(&mut self, new_value: impl AsRef<str>) -> Result<(), Err> {
        *self = Self::parse(new_value)?;
        Ok(())
    }

    /// Get the inbox.
    pub fn inbox(&self) -> Option<&InboxId> {
        self.inbox.as_ref()
    }

    /// The canonical form of the UserId. The value is already stored as such.
    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
    pub fn canonical(&self) -> String {
        if let Some(inbox) = &self.inbox {
            format!("{}#{}", inbox.canonical(), self.id)
        } else {
            format!("#{}", self.id)
        }
    }
}

impl std::fmt::Display for UserId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.canonical())
    }
}

impl Serialize for UserId {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.canonical())
    }
}

impl<'de> Deserialize<'de> for UserId {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct UserIdVisitor;

        impl<'de> Visitor<'de> for UserIdVisitor {
            type Value = UserId;

            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                f.write_str("a canonical Relay Mail address string")
            }

            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                UserId::parse(value)
                    .map_err(|e| de::Error::custom(format!("invalid address `{}`: {}", value, e)))
            }

            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
            where
                E: de::Error,
            {
                self.visit_str(&value)
            }
        }

        deserializer.deserialize_str(UserIdVisitor)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_and_canonicalizes() {
        assert_eq!(
            UserId::parse("Alice.Smith").unwrap().canonical(),
            "#alice.smith"
        );
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidUser")]
    fn rejects_empty() {
        let _ = UserId::parse("").unwrap();
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
    fn rejects_invalid_chars() {
        let _ = UserId::parse("Invalid User!").unwrap();
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
    fn reject_untrimmed() {
        let _ = UserId::parse("  trim_me  ").unwrap();
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
    fn rejects_non_ascii() {
        let _ = UserId::parse("调试输出").unwrap();
    }

    #[test]
    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
    fn rejects_homoglyphs() {
        let _ = UserId::parse("аlice").unwrap(); // Cyrillic 'а'
    }
}