Skip to main content

relay_core/identity/
user.rs

1use serde::{
2    Deserialize, Deserializer, Serialize, Serializer,
3    de::{self, Visitor},
4};
5
6use crate::prelude::Address;
7
8use super::{IdentityError as Err, canonical_identity_string, is_valid_identity_string};
9
10/// A unique identifier for a User within an Agent's domain.
11/// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#user-identity
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct UserId(String);
14
15impl From<Address> for UserId {
16    fn from(address: Address) -> Self {
17        address.user.clone()
18    }
19}
20
21impl TryFrom<&str> for UserId {
22    type Error = Err;
23
24    fn try_from(value: &str) -> Result<Self, Err> {
25        UserId::parse(value)
26    }
27}
28
29impl TryFrom<String> for UserId {
30    type Error = Err;
31
32    fn try_from(value: String) -> Result<Self, Err> {
33        UserId::parse(value)
34    }
35}
36
37impl UserId {
38    /// Parse a UserId from a string according to the identity rules.
39    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#user-identity
40    ///
41    /// # Example
42    /// ```
43    /// let user_id = relay_core::id::UserId::parse("Alice.Smith").unwrap();
44    /// assert_eq!(user_id.canonical(), "alice.smith");
45    /// ```
46    pub fn parse(input: impl AsRef<str>) -> Result<Self, Err> {
47        let input = input.as_ref();
48
49        if input.is_empty() {
50            return Err(Err::InvalidUser);
51        }
52        if !is_valid_identity_string(input) {
53            return Err(Err::InvalidIdentityString);
54        }
55
56        let canonical = canonical_identity_string(input);
57
58        Ok(Self(canonical))
59    }
60
61    /// Replace the current UserId with a new value.
62    pub fn replace(&mut self, new_value: impl AsRef<str>) -> Result<(), Err> {
63        *self = Self::parse(new_value)?;
64        Ok(())
65    }
66
67    /// The canonical form of the UserId. The value is already stored as such.
68    /// https://gitlab.com/relay-mail/docs/-/wikis/Architecture/Identity#canonical-form
69    pub fn canonical(&self) -> &str {
70        &self.0
71    }
72}
73
74impl std::fmt::Display for UserId {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{}", self.canonical())
77    }
78}
79
80impl Serialize for UserId {
81    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
82    where
83        S: Serializer,
84    {
85        serializer.serialize_str(self.canonical())
86    }
87}
88
89impl<'de> Deserialize<'de> for UserId {
90    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91    where
92        D: Deserializer<'de>,
93    {
94        struct UserIdVisitor;
95
96        impl<'de> Visitor<'de> for UserIdVisitor {
97            type Value = UserId;
98
99            fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
100                f.write_str("a canonical Relay Mail address string")
101            }
102
103            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
104            where
105                E: de::Error,
106            {
107                UserId::parse(value)
108                    .map_err(|e| de::Error::custom(format!("invalid address `{}`: {}", value, e)))
109            }
110
111            fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
112            where
113                E: de::Error,
114            {
115                self.visit_str(&value)
116            }
117        }
118
119        deserializer.deserialize_str(UserIdVisitor)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn parses_and_canonicalizes() {
129        assert_eq!(
130            UserId::parse("Alice.Smith").unwrap().canonical(),
131            "alice.smith"
132        );
133    }
134
135    #[test]
136    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidUser")]
137    fn rejects_empty() {
138        let _ = UserId::parse("").unwrap();
139    }
140
141    #[test]
142    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
143    fn rejects_invalid_chars() {
144        let _ = UserId::parse("Invalid User!").unwrap();
145    }
146
147    #[test]
148    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
149    fn reject_untrimmed() {
150        let _ = UserId::parse("  trim_me  ").unwrap();
151    }
152
153    #[test]
154    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
155    fn rejects_non_ascii() {
156        let _ = UserId::parse("调试输出").unwrap();
157    }
158
159    #[test]
160    #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: InvalidIdentityString")]
161    fn rejects_homoglyphs() {
162        let _ = UserId::parse("аlice").unwrap(); // Cyrillic 'а'
163    }
164}