trakt_core/
emoji_str.rs

1use std::{fmt::Formatter, ops::Deref};
2
3use serde::{de::Error, Deserialize, Deserializer};
4
5/// A string that deserializes strings containing emoji shortcodes into their respective unicode
6/// characters.
7///
8/// Use `EmojiString::from` to create a new instance of `EmojiString` from a `&str`, replacing any
9/// emoji shortcodes with their respective unicode characters.
10#[derive(Debug, Clone, Eq, PartialEq, Hash)]
11pub struct EmojiString(String);
12
13impl Deref for EmojiString {
14    type Target = str;
15
16    fn deref(&self) -> &Self::Target {
17        &self.0
18    }
19}
20
21impl From<&str> for EmojiString {
22    fn from(mut value: &str) -> Self {
23        let mut o = String::new();
24
25        // Shamelessly stolen from:
26        // https://github.com/rossmacarthur/emojis/blob/b088b129c59124df8c3d7c3a5aff116114c78acf/examples/replace.rs#L23
27        // The meaning of the index values is as follows.
28        //
29        //  : r o c k e t :
30        // ^ ^           ^ ^
31        // i m           n j
32        //
33        // i..j gives ":rocket:"
34        // m..n gives "rocket"
35        while let Some((i, m, n, j)) = value
36            .find(':')
37            .map(|i| (i, i + 1))
38            .and_then(|(i, m)| value[m..].find(':').map(|x| (i, m, m + x, m + x + 1)))
39        {
40            if let Some(emoji) = emojis::get_by_shortcode(&value[m..n]) {
41                // Output everything preceding, except the first colon
42                o.push_str(&value[..i]);
43                // Output the emoji.
44                o.push_str(emoji.as_str());
45                // Update the string to past the last colon.
46                value = &value[j..];
47            } else {
48                // Output everything preceding but not including the colon
49                o.push_str(&value[..n]);
50                // Update the string to start with the last colon
51                value = &value[n..];
52            }
53        }
54
55        o.push_str(value);
56        Self(o)
57    }
58}
59
60impl From<EmojiString> for String {
61    fn from(value: EmojiString) -> Self {
62        value.0
63    }
64}
65
66impl<'de> Deserialize<'de> for EmojiString {
67    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
68    where
69        D: Deserializer<'de>,
70    {
71        struct Visitor;
72
73        impl<'de> serde::de::Visitor<'de> for Visitor {
74            type Value = EmojiString;
75
76            fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
77                formatter.write_str("a string containing emoji shortcodes")
78            }
79
80            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
81            where
82                E: Error,
83            {
84                Ok(v.into())
85            }
86
87            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
88            where
89                E: Error,
90            {
91                let s = std::str::from_utf8(v)
92                    .map_err(|_| E::invalid_value(serde::de::Unexpected::Bytes(v), &self))?;
93                Ok(s.into())
94            }
95        }
96
97        deserializer.deserialize_str(Visitor)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    pub fn test_from_str() {
107        let tests = [
108            ("launch nothing", "launch nothing"),
109            ("launch :rocket: something", "launch 🚀 something"),
110            ("? :unknown: emoji", "? :unknown: emoji"),
111            ("::very:naughty::", "::very:naughty::"),
112            (":maybe:rocket:", ":maybe🚀"),
113            (":rocket::rocket:", "🚀🚀"),
114        ];
115
116        for (i, o) in tests {
117            let i: EmojiString = i.into();
118            assert_eq!(&*i, o);
119        }
120    }
121
122    #[test]
123    pub fn test_deserialize() {
124        let tests = [
125            ("launch nothing", "launch nothing"),
126            ("launch :rocket: something", "launch 🚀 something"),
127            ("? :unknown: emoji", "? :unknown: emoji"),
128            ("::very:naughty::", "::very:naughty::"),
129            (":maybe:rocket:", ":maybe🚀"),
130            (":rocket::rocket:", "🚀🚀"),
131        ];
132
133        for (i, o) in tests {
134            let i: EmojiString = serde_json::from_str(&format!("\"{i}\"")).unwrap();
135            assert_eq!(&*i, o);
136        }
137    }
138}