xapi_rs/data/
email_address.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::data::{DataError, Fingerprint};
4use core::fmt;
5use email_address::EmailAddress;
6use serde::{
7    de::{self, Visitor},
8    Deserialize, Deserializer, Serialize, Serializer,
9};
10use std::{
11    cmp::Ordering,
12    hash::{Hash, Hasher},
13    str::FromStr,
14};
15
16/// Implementation of Email-Address that wraps [EmailAddress] to better satisfy
17/// the requirements of xAPI while reducing the verbosity making the mandatory
18/// `mailto:` scheme prefix optional.
19#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
20pub struct MyEmailAddress(
21    #[serde(serialize_with = "mbox_ser", deserialize_with = "mbox_des")] EmailAddress,
22);
23
24impl MyEmailAddress {
25    pub(crate) fn from(email: EmailAddress) -> Self {
26        MyEmailAddress(email)
27    }
28
29    /// Return this email address formatted as a URI. Will also URI-encode the
30    /// address itself. So, `name@example.org` becomes `mailto:name@example.org`.
31    pub fn to_uri(&self) -> String {
32        self.0.to_uri()
33    }
34}
35
36impl FromStr for MyEmailAddress {
37    type Err = DataError;
38
39    fn from_str(s: &str) -> Result<Self, Self::Err> {
40        let email = if let Some(x) = s.strip_prefix("mailto:") {
41            x
42        } else {
43            s
44        };
45        Ok(MyEmailAddress::from(EmailAddress::from_str(email)?))
46    }
47}
48
49impl fmt::Display for MyEmailAddress {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", self.0)
52    }
53}
54
55impl Fingerprint for MyEmailAddress {
56    fn fingerprint<H: Hasher>(&self, state: &mut H) {
57        self.0.as_str().to_lowercase().hash(state);
58    }
59}
60
61impl PartialOrd for MyEmailAddress {
62    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
63        self.0
64            .as_str()
65            .to_lowercase()
66            .partial_cmp(&other.0.as_str().to_lowercase())
67    }
68}
69
70impl AsRef<EmailAddress> for MyEmailAddress {
71    fn as_ref(&self) -> &EmailAddress {
72        &self.0
73    }
74}
75
76/// `serde` JSON deserialization visitor for correctly parsing email addresses
77/// whether or not they are prefixed w/ the `mailto` scheme.
78struct EmailAddressVisitor;
79
80impl Visitor<'_> for EmailAddressVisitor {
81    type Value = EmailAddress;
82
83    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
84        formatter.write_str("an [mailto:]email-address")
85    }
86
87    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
88    where
89        E: de::Error,
90    {
91        let email = if let Some(x) = value.strip_prefix("mailto:") {
92            x
93        } else {
94            value
95        };
96        match EmailAddress::from_str(email) {
97            Ok(x) => Ok(x),
98            Err(x) => Err(de::Error::custom(x)),
99        }
100    }
101}
102
103/// Serializer implementation for the wrapped EMail type.
104fn mbox_ser<S>(this: &EmailAddress, ser: S) -> Result<S::Ok, S::Error>
105where
106    S: Serializer,
107{
108    ser.serialize_str(this.to_uri().as_str())
109}
110
111/// Deserializer implementation for the wrapped Email type.
112fn mbox_des<'de, D>(des: D) -> Result<EmailAddress, D::Error>
113where
114    D: Deserializer<'de>,
115{
116    des.deserialize_str(EmailAddressVisitor)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_mbox_serde() {
125        #[derive(Debug, Deserialize, Serialize)]
126        struct Foo {
127            mbox: Option<MyEmailAddress>,
128        }
129
130        const IN1: &str = r#"{ }"#;
131        let r1 = serde_json::from_str::<Foo>(&IN1);
132        assert!(r1.is_ok());
133        let v1 = r1.unwrap().mbox;
134        assert!(v1.is_none());
135
136        const IN2: &str = r#"{ "mbox": "example.learner@adlnet.gov" }"#;
137        let r2 = serde_json::from_str::<Foo>(&IN2);
138        assert!(r2.is_ok());
139        let v2 = r2.unwrap().mbox;
140
141        const IN3: &str = r#"{ "mbox": "mailto:example.learner@adlnet.gov" }"#;
142        let r3 = serde_json::from_str::<Foo>(&IN3);
143        assert!(r3.is_ok());
144        let v3 = r3.unwrap().mbox;
145
146        assert_eq!(v2, v3);
147
148        const IN4: &str = r#"{ "mbox": "example.learner_adlnet.gov" }"#;
149        let r4 = serde_json::from_str::<Foo>(&IN4);
150        assert!(r4.is_err());
151
152        const IN5: &str = r#"{ "mbox": "mailto:example.learner_adlnet.gov" }"#;
153        let r5 = serde_json::from_str::<Foo>(&IN5);
154        assert!(r5.is_err());
155    }
156
157    #[test]
158    fn test_email_eq() {
159        let em1 = EmailAddress::from_str("me@gmailbox.net");
160        let em2 = EmailAddress::from_str("me@gmailbox.net");
161        assert_eq!(em1, em2)
162    }
163}