Skip to main content

relay_core/identity/
user.rs

1use serde::{Deserialize, Serialize};
2
3use crate::prelude::Address;
4
5use super::{IdentityError as Err, canonical_identity_string, is_valid_identity_string};
6
7/// A unique identifier for a User within an Agent's domain.
8/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#user-identity
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct UserId(String);
11
12impl From<Address> for UserId {
13    fn from(address: Address) -> Self {
14        address.user.clone()
15    }
16}
17
18impl TryFrom<&str> for UserId {
19    type Error = Err;
20
21    fn try_from(value: &str) -> Result<Self, Err> {
22        UserId::parse(value)
23    }
24}
25
26impl TryFrom<String> for UserId {
27    type Error = Err;
28
29    fn try_from(value: String) -> Result<Self, Err> {
30        UserId::parse(value)
31    }
32}
33
34impl UserId {
35    /// Parse a UserId from a string according to the identity rules.
36    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#user-identity
37    ///
38    /// # Example
39    /// ```
40    /// let user_id = relay_core::id::UserId::parse("Alice.Smith").unwrap();
41    /// assert_eq!(user_id.canonical(), "alice.smith");
42    /// ```
43    pub fn parse(input: impl AsRef<str>) -> Result<Self, Err> {
44        let input = input.as_ref();
45
46        if input.is_empty() {
47            return Err(Err::InvalidUser);
48        }
49        if !is_valid_identity_string(input) {
50            return Err(Err::InvalidIdentityString);
51        }
52
53        let canonical = canonical_identity_string(input);
54
55        Ok(Self(canonical))
56    }
57
58    /// Replace the current UserId with a new value.
59    pub fn replace(&mut self, new_value: impl AsRef<str>) -> Result<(), Err> {
60        *self = Self::parse(new_value)?;
61        Ok(())
62    }
63
64    /// The canonical form of the UserId. The value is already stored as such.
65    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
66    pub fn canonical(&self) -> &str {
67        &self.0
68    }
69}
70
71impl std::fmt::Display for UserId {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        write!(f, "{}", self.canonical())
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn parses_and_canonicalizes() {
83        assert_eq!(
84            UserId::parse("Alice.Smith").unwrap().canonical(),
85            "alice.smith"
86        );
87    }
88
89    #[test]
90    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidUser")]
91    fn rejects_empty() {
92        let _ = UserId::parse("").unwrap();
93    }
94
95    #[test]
96    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
97    fn rejects_invalid_chars() {
98        let _ = UserId::parse("Invalid User!").unwrap();
99    }
100
101    #[test]
102    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
103    fn reject_untrimmed() {
104        let _ = UserId::parse("  trim_me  ").unwrap();
105    }
106
107    #[test]
108    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
109    fn rejects_non_ascii() {
110        let _ = UserId::parse("调试输出").unwrap();
111    }
112
113    #[test]
114    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
115    fn rejects_homoglyphs() {
116        let _ = UserId::parse("аlice").unwrap(); // Cyrillic 'а'
117    }
118}