relay-core 0.1.0-alpha.5

The core components of the Relay Protocol.
Documentation
use serde::{Deserialize, Serialize};

use crate::prelude::Address;

use super::{IdentityError as Err, canonical_identity_string, is_valid_identity_string};

/// A unique identifier for an Inbox.
/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#inbox
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct InboxId(String);

impl TryFrom<Address> for InboxId {
    type Error = Err;

    fn try_from(address: Address) -> Result<Self, Err> {
        match address.inbox {
            Some(inbox) => Ok(inbox),
            None => Err(Err::InvalidInbox),
        }
    }
}

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

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

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

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

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

        if input.is_empty() {
            return Err(Err::InvalidInbox);
        }
        if !is_valid_identity_string(input) {
            return Err(Err::InvalidIdentityString);
        }

        let canonical = canonical_identity_string(input);

        Ok(Self(canonical))
    }

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

    /// The canonical form of the InboxId. The value is already stored as such.
    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
    pub fn canonical(&self) -> &str {
        &self.0
    }
}

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

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

    #[test]
    fn parses_and_canonicalizes() {
        assert_eq!(InboxId::parse("Work").unwrap().canonical(), "work");
    }

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

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

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

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

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