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