twilight_model/gateway/presence/
activity_button.rs

1//! Representations of activity linked or textual buttons.
2
3use serde::{
4    de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
5    ser::{SerializeStruct, Serializer},
6    Deserialize, Serialize,
7};
8use std::fmt::{Formatter, Result as FmtResult};
9
10/// Button used in an activity.
11///
12/// # serde
13///
14/// Activity buttons with a URL deserialize and serialize as a struct:
15///
16/// ```
17/// use twilight_model::gateway::presence::activity_button::{ActivityButton, ActivityButtonLink};
18///
19/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
20/// const JSON: &str = r#"{
21///     "label": "a",
22///     "url": "b"
23/// }"#;
24///
25/// assert_eq!(
26///     ActivityButton::Link(ActivityButtonLink {
27///         label: "a".to_owned(),
28///         url: "b".to_owned(),
29///     }),
30///     serde_json::from_str(JSON)?,
31/// );
32/// # Ok(()) }
33/// ```
34///
35/// An activity button without a URL - an [`ActivityButtonText`] - will
36/// deserialize and serialize as a string. This means that a textual activity
37/// button with a label of "test" will serialize as simply the string "test" and
38/// vice versa.
39///
40/// ```
41/// use twilight_model::gateway::presence::activity_button::{ActivityButton, ActivityButtonText};
42///
43/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
44/// assert_eq!(
45///     ActivityButton::Text(ActivityButtonText {
46///         label: "test".to_owned(),
47///     }),
48///     serde_json::from_str(r#""test""#)?,
49/// );
50/// # Ok(()) }
51/// ```
52#[derive(Clone, Debug, Eq, Hash, PartialEq)]
53pub enum ActivityButton {
54    /// Activity button is a link.
55    Link(ActivityButtonLink),
56    /// Activity button is textual.
57    Text(ActivityButtonText),
58    /// Variant value is unknown to the library.
59    Unknown,
60}
61
62impl ActivityButton {
63    /// Whether the variant is a link button.
64    pub const fn is_link(&self) -> bool {
65        matches!(self, Self::Link(_))
66    }
67
68    /// Whether the variant is a text button.
69    pub const fn is_text(&self) -> bool {
70        matches!(self, Self::Text(_))
71    }
72
73    /// Retrieve an immutable reference to the label.
74    #[allow(clippy::missing_const_for_fn)]
75    pub fn label(&self) -> Option<&str> {
76        match self {
77            Self::Link(link) => Some(&link.label),
78            Self::Text(text) => Some(&text.label),
79            Self::Unknown => None,
80        }
81    }
82
83    /// Retrieve an immutable reference to the URL if this is a link activity
84    /// button.
85    #[allow(clippy::missing_const_for_fn)]
86    pub fn url(&self) -> Option<&str> {
87        if let Self::Link(link) = self {
88            Some(&link.url)
89        } else {
90            None
91        }
92    }
93}
94
95#[derive(Debug, Deserialize)]
96#[serde(field_identifier, rename_all = "snake_case")]
97enum ActivityButtonField {
98    Label,
99    Url,
100}
101
102struct ActivityButtonVisitor;
103
104impl<'de> Visitor<'de> for ActivityButtonVisitor {
105    type Value = ActivityButton;
106
107    fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
108        f.write_str("activity button struct or string")
109    }
110
111    fn visit_string<E: DeError>(self, v: String) -> Result<Self::Value, E> {
112        Ok(ActivityButton::Text(ActivityButtonText { label: v }))
113    }
114
115    fn visit_str<E: DeError>(self, v: &str) -> Result<Self::Value, E> {
116        Ok(ActivityButton::Text(ActivityButtonText {
117            label: v.to_owned(),
118        }))
119    }
120
121    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
122        let mut label = None;
123        let mut url = None;
124
125        loop {
126            let key = match map.next_key() {
127                Ok(Some(key)) => key,
128                Ok(None) => break,
129                Err(_) => {
130                    map.next_value::<IgnoredAny>()?;
131
132                    continue;
133                }
134            };
135
136            match key {
137                ActivityButtonField::Label => {
138                    if label.is_some() {
139                        return Err(DeError::duplicate_field("label"));
140                    }
141
142                    label = Some(map.next_value()?);
143                }
144                ActivityButtonField::Url => {
145                    if url.is_some() {
146                        return Err(DeError::duplicate_field("url"));
147                    }
148
149                    url = Some(map.next_value()?);
150                }
151            }
152        }
153
154        let label = label.ok_or_else(|| DeError::missing_field("label"))?;
155        let url = url.ok_or_else(|| DeError::missing_field("url"))?;
156
157        Ok(ActivityButton::Link(ActivityButtonLink { label, url }))
158    }
159}
160
161impl<'de> Deserialize<'de> for ActivityButton {
162    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
163        deserializer.deserialize_any(ActivityButtonVisitor)
164    }
165}
166
167impl Serialize for ActivityButton {
168    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
169        match self {
170            Self::Link(link) => {
171                let mut state = serializer.serialize_struct("ActivityButton", 2)?;
172
173                state.serialize_field("label", &link.label)?;
174                state.serialize_field("url", &link.url)?;
175
176                state.end()
177            }
178            Self::Text(text) => serializer.serialize_str(&text.label),
179            Self::Unknown => Err(serde::ser::Error::custom(
180                "Can't serialize an unknown activity button type",
181            )),
182        }
183    }
184}
185
186/// Button used in an activity with a URL.
187#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
188pub struct ActivityButtonLink {
189    /// Text shown on the button.
190    pub label: String,
191    /// URL opened when clicking the button.
192    pub url: String,
193}
194
195/// Button used in an activity without a URL.
196///
197/// # serde
198///
199/// Textual activity buttons deserialize and serialize as a string. This means
200/// that a textual activity button with a label of "test" will serialize as
201/// simply the string "test" and vice versa.
202///
203/// ```ignore
204/// use twilight_model::gateway::presence::activity_button::ActivityButtonText;
205///
206/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
207/// assert_eq!(
208///     ActivityButtonText { label: "test".to_owned() },
209///     serde_json::from_str(r#""test""#)?,
210/// );
211/// # Ok(()) }
212/// ```
213#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
214#[serde(transparent)]
215pub struct ActivityButtonText {
216    /// Text shown on the button.
217    pub label: String,
218}
219
220#[cfg(test)]
221mod tests {
222    use super::{ActivityButton, ActivityButtonLink, ActivityButtonText};
223    use serde::{Deserialize, Serialize};
224    use serde_test::Token;
225    use static_assertions::{assert_fields, assert_impl_all};
226    use std::fmt::Debug;
227
228    assert_fields!(ActivityButtonLink: label, url);
229    assert_impl_all!(
230        ActivityButtonLink: Clone,
231        Debug,
232        Deserialize<'static>,
233        Eq,
234        PartialEq,
235        Serialize
236    );
237    assert_fields!(ActivityButtonText: label);
238    assert_impl_all!(
239        ActivityButtonText: Clone,
240        Debug,
241        Deserialize<'static>,
242        Eq,
243        PartialEq,
244        Serialize
245    );
246    assert_impl_all!(
247        ActivityButton: Clone,
248        Debug,
249        Deserialize<'static>,
250        Eq,
251        PartialEq,
252        Serialize
253    );
254
255    fn link() -> ActivityButtonLink {
256        ActivityButtonLink {
257            label: "a".to_owned(),
258            url: "b".to_owned(),
259        }
260    }
261
262    fn text() -> ActivityButtonText {
263        ActivityButtonText {
264            label: "a".to_owned(),
265        }
266    }
267
268    #[test]
269    fn activity_button_link() {
270        serde_test::assert_de_tokens(
271            &link(),
272            &[
273                Token::Struct {
274                    name: "ActivityButtonLink",
275                    len: 2,
276                },
277                Token::Str("label"),
278                Token::Str("a"),
279                Token::Str("url"),
280                Token::Str("b"),
281                Token::StructEnd,
282            ],
283        );
284    }
285
286    #[test]
287    fn activity_button_text() {
288        serde_test::assert_de_tokens(&text(), &[Token::Str("a")]);
289    }
290
291    #[test]
292    fn activity_button_with_url() {
293        serde_test::assert_tokens(
294            &ActivityButton::Link(link()),
295            &[
296                Token::Struct {
297                    name: "ActivityButton",
298                    len: 2,
299                },
300                Token::Str("label"),
301                Token::Str("a"),
302                Token::Str("url"),
303                Token::Str("b"),
304                Token::StructEnd,
305            ],
306        );
307    }
308
309    #[test]
310    fn activity_button_without_url() {
311        serde_test::assert_tokens(&ActivityButton::Text(text()), &[Token::Str("a")]);
312    }
313}