Skip to main content

slack_messaging/composition_objects/
text.rs

1use crate::validators::*;
2
3use serde::{Serialize, Serializer};
4use slack_messaging_derive::Builder;
5
6/// [Text object](https://docs.slack.dev/reference/block-kit/composition-objects/text-object)
7/// representation.
8///
9/// This is a generic struct that can represent either a plain text object or a markdown text
10/// object, depending on the type parameter `T`.
11///
12/// # Type Parameters
13///
14/// * `T`: The type of text object. It can be either [`Plain`] for plain text or [`Mrkdwn`] for
15///   markdown text.
16///
17/// # Fields and Validations
18///
19/// For more details, see the [Slack API
20/// documentation](https://docs.slack.dev/reference/block-kit/composition-objects/text-object).
21///
22/// ## `TextObject<Plain>` or alias `PlainText`
23///
24/// | Field | Type | Required | Validation |
25/// |-------|------|----------|------------|
26/// | text | String | Yes | Minimum length: 1 character, Maximum length: 3000 characters. |
27/// | emoji | bool | No | N/A |
28///
29/// ## `TextObject<Mrkdwn>` or alias `MrkdwnText`
30///
31/// | Field | Type | Required | Validation |
32/// |-------|------|----------|------------|
33/// | text | String | Yes | Minimum length: 1 character, Maximum length: 3000 characters. |
34/// | verbatim | bool | No | N/A |
35///
36/// # Example
37///
38///```
39/// use slack_messaging::composition_objects::{PlainText, MrkdwnText};
40/// # use std::error::Error;
41/// # fn try_main() -> Result<(), Box<dyn Error>> {
42///
43/// // 1. Plain Text Object
44/// let plain_text = PlainText::builder()
45///     .text("Hello, World!")
46///     .emoji(true)
47///     .build()?;
48///
49/// let plain_json = serde_json::to_value(plain_text).unwrap();
50///
51/// let expected_plain = serde_json::json!({
52///     "type": "plain_text",
53///     "text": "Hello, World!",
54///     "emoji": true
55/// });
56///
57/// assert_eq!(plain_json, expected_plain);
58///
59/// // 2. Markdown Text Object
60/// let mrkdwn_text = MrkdwnText::builder()
61///     .text("*Hello*, _World_!")
62///     .verbatim(false)
63///     .build()?;
64///
65/// let mrkdwn_json = serde_json::to_value(mrkdwn_text).unwrap();
66///
67/// let expected_mrkdwn = serde_json::json!({
68///     "type": "mrkdwn",
69///     "text": "*Hello*, _World_!",
70///     "verbatim": false
71/// });
72///
73/// assert_eq!(mrkdwn_json, expected_mrkdwn);
74/// #     Ok(())
75/// # }
76/// # fn main() {
77/// #     try_main().unwrap()
78/// # }
79///```
80#[derive(Debug, Clone, Builder)]
81pub struct TextObject<T> {
82    #[builder(phantom = "T")]
83    pub(crate) r#type: std::marker::PhantomData<T>,
84
85    #[builder(validate("required", "text::min_1", "text::max_3000"))]
86    pub(crate) text: Option<String>,
87
88    #[builder(no_accessors)]
89    pub(crate) emoji: Option<bool>, // for PlainText
90
91    #[builder(no_accessors)]
92    pub(crate) verbatim: Option<bool>, // for MrkdwnText
93}
94
95/// A trait for types that have a `text` field, allowing access to the text content.
96pub trait TextExt {
97    fn text(&self) -> Option<&str>;
98}
99
100impl<T> TextExt for TextObject<T> {
101    /// get text field value.
102    fn text(&self) -> Option<&str> {
103        self.text.as_deref()
104    }
105}
106
107/// Text object of type "plain_text".
108#[derive(Debug, Clone, Copy, PartialEq)]
109pub struct Plain;
110
111/// Type alias for a plain text object.
112pub type PlainText = TextObject<Plain>;
113
114/// Text object of type "mrkdwn".
115#[derive(Debug, Clone, Copy, PartialEq)]
116pub struct Mrkdwn;
117
118/// Type alias for a markdown text object.
119pub type MrkdwnText = TextObject<Mrkdwn>;
120
121impl TextObjectBuilder<Plain> {
122    /// get emoji field value.
123    pub fn get_emoji(&self) -> Option<bool> {
124        self.emoji.inner_ref().copied()
125    }
126
127    /// set emoji field value.
128    pub fn set_emoji(self, emoji: Option<impl Into<bool>>) -> TextObjectBuilder<Plain> {
129        Self {
130            emoji: Self::new_emoji(emoji.map(|v| v.into())),
131            ..self
132        }
133    }
134
135    /// set emoji field value.
136    pub fn emoji(self, emoji: impl Into<bool>) -> TextObjectBuilder<Plain> {
137        self.set_emoji(Some(emoji))
138    }
139}
140
141impl TextObjectBuilder<Mrkdwn> {
142    /// get verbatim field value.
143    pub fn get_verbatim(&self) -> Option<bool> {
144        self.verbatim.inner_ref().copied()
145    }
146
147    /// set verbatim field value.
148    pub fn set_verbatim(self, verbatim: Option<impl Into<bool>>) -> TextObjectBuilder<Mrkdwn> {
149        Self {
150            verbatim: Self::new_verbatim(verbatim.map(|v| v.into())),
151            ..self
152        }
153    }
154
155    /// set verbatim field value.
156    pub fn verbatim(self, verbatim: impl Into<bool>) -> TextObjectBuilder<Mrkdwn> {
157        self.set_verbatim(Some(verbatim))
158    }
159}
160
161impl PartialEq for TextObject<Plain> {
162    fn eq(&self, other: &Self) -> bool {
163        match (self.text(), other.text()) {
164            (Some(text1), Some(text2)) if text1 != text2 => false,
165            (None, Some(_)) | (Some(_), None) => false,
166            _ => self.emoji.unwrap_or(false) == other.emoji.unwrap_or(false),
167        }
168    }
169}
170
171impl PartialEq for TextObject<Mrkdwn> {
172    fn eq(&self, other: &Self) -> bool {
173        match (self.text(), other.text()) {
174            (Some(text1), Some(text2)) if text1 != text2 => false,
175            (None, Some(_)) | (Some(_), None) => false,
176            _ => self.verbatim.unwrap_or(false) == other.verbatim.unwrap_or(false),
177        }
178    }
179}
180
181impl Serialize for TextObject<Plain> {
182    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
183    where
184        S: Serializer,
185    {
186        use serde::ser::SerializeStruct;
187
188        let mut state = serializer.serialize_struct("Text", 3)?;
189
190        state.serialize_field("type", "plain_text")?;
191
192        if let Some(text) = &self.text {
193            state.serialize_field("text", text)?;
194        }
195
196        if let Some(emoji) = &self.emoji {
197            state.serialize_field("emoji", emoji)?;
198        }
199
200        state.end()
201    }
202}
203
204impl Serialize for TextObject<Mrkdwn> {
205    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
206    where
207        S: Serializer,
208    {
209        use serde::ser::SerializeStruct;
210
211        let mut state = serializer.serialize_struct("Text", 3)?;
212
213        state.serialize_field("type", "mrkdwn")?;
214
215        if let Some(text) = &self.text {
216            state.serialize_field("text", text)?;
217        }
218
219        if let Some(verbatim) = &self.verbatim {
220            state.serialize_field("verbatim", verbatim)?;
221        }
222
223        state.end()
224    }
225}
226
227/// Enum representation of Text objects.
228/// Use this when you need to handle both [`Plain`] and [`Mrkdwn`] text objects.
229#[derive(Debug, Clone, Serialize, PartialEq)]
230#[serde(untagged)]
231pub enum Text {
232    /// Plain text object.
233    Plain(TextObject<Plain>),
234    /// Markdown text object.
235    Mrkdwn(TextObject<Mrkdwn>),
236}
237
238impl TextExt for Text {
239    /// get text field value.
240    fn text(&self) -> Option<&str> {
241        match self {
242            Text::Plain(t) => t.text(),
243            Text::Mrkdwn(t) => t.text(),
244        }
245    }
246}
247
248impl TextExt for String {
249    fn text(&self) -> Option<&str> {
250        Some(self.as_str())
251    }
252}
253
254impl From<TextObject<Plain>> for Text {
255    fn from(text: TextObject<Plain>) -> Self {
256        Text::Plain(text)
257    }
258}
259
260impl From<TextObject<Mrkdwn>> for Text {
261    fn from(text: TextObject<Mrkdwn>) -> Self {
262        Text::Mrkdwn(text)
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::errors::*;
270
271    #[test]
272    fn it_requires_text() {
273        let err = PlainText::builder().build().unwrap_err();
274        assert_eq!(err.object(), "TextObject");
275        let text_err = err.field("text");
276        assert!(text_err.includes(ValidationErrorKind::Required));
277    }
278
279    #[test]
280    fn it_requires_text_more_than_1_character() {
281        let err = PlainText::builder().text("").build().unwrap_err();
282        assert_eq!(err.object(), "TextObject");
283        let text_err = err.field("text");
284        assert!(text_err.includes(ValidationErrorKind::MinTextLength(1)));
285    }
286
287    #[test]
288    fn it_requires_text_less_than_3000_characters() {
289        let err = PlainText::builder()
290            .text("a".repeat(3001))
291            .build()
292            .unwrap_err();
293        assert_eq!(err.object(), "TextObject");
294        let text_err = err.field("text");
295        assert!(text_err.includes(ValidationErrorKind::MaxTextLength(3000)));
296    }
297
298    mod plain_text {
299        use super::*;
300
301        #[test]
302        fn it_implements_builder() {
303            let expected = PlainText {
304                r#type: std::marker::PhantomData,
305                text: Some("Hello World:smile:".into()),
306                emoji: Some(true),
307                verbatim: None,
308            };
309
310            let text = PlainText::builder()
311                .text("Hello World:smile:")
312                .emoji(true)
313                .build()
314                .unwrap();
315
316            assert_eq!(text, expected);
317
318            let text = PlainText::builder()
319                .set_text(Some("Hello World:smile:"))
320                .set_emoji(Some(true))
321                .build()
322                .unwrap();
323
324            assert_eq!(text, expected);
325        }
326
327        #[test]
328        fn it_serializes_to_json() {
329            let text = PlainText {
330                r#type: std::marker::PhantomData,
331                text: Some("Hello World :smile:".into()),
332                emoji: Some(true),
333                verbatim: None,
334            };
335
336            let expected = serde_json::json!({
337                "type": "plain_text",
338                "text": "Hello World :smile:",
339                "emoji": true
340            });
341            let json = serde_json::to_value(&text).unwrap();
342            assert_eq!(json, expected);
343        }
344
345        #[test]
346        fn it_equals_another_plain_text() {
347            let text1 = PlainText {
348                r#type: std::marker::PhantomData,
349                text: Some("Hello".into()),
350                emoji: Some(true),
351                verbatim: None,
352            };
353            let text2 = PlainText {
354                r#type: std::marker::PhantomData,
355                text: Some("Hello".into()),
356                emoji: Some(true),
357                verbatim: None,
358            };
359            let text3 = PlainText {
360                r#type: std::marker::PhantomData,
361                text: Some("Hello".into()),
362                emoji: Some(false),
363                verbatim: None,
364            };
365            let text4 = PlainText {
366                r#type: std::marker::PhantomData,
367                text: Some("World".into()),
368                emoji: Some(true),
369                verbatim: None,
370            };
371            let text5 = PlainText {
372                r#type: std::marker::PhantomData,
373                text: Some("Hello".into()),
374                emoji: None,
375                verbatim: None,
376            };
377            assert_eq!(text1, text2);
378            assert_ne!(text1, text3);
379            assert_ne!(text1, text4);
380            assert_eq!(text3, text5);
381        }
382    }
383
384    mod mrkdwn_text {
385        use super::*;
386
387        #[test]
388        fn it_implements_builder() {
389            let expected = MrkdwnText {
390                r#type: std::marker::PhantomData,
391                text: Some("*Hello* _World_ :smile:".into()),
392                emoji: None,
393                verbatim: Some(false),
394            };
395
396            let text = MrkdwnText::builder()
397                .text("*Hello* _World_ :smile:")
398                .verbatim(false)
399                .build()
400                .unwrap();
401
402            assert_eq!(text, expected);
403
404            let text = MrkdwnText::builder()
405                .set_text(Some("*Hello* _World_ :smile:"))
406                .set_verbatim(Some(false))
407                .build()
408                .unwrap();
409
410            assert_eq!(text, expected);
411        }
412
413        #[test]
414        fn it_serializes_to_json() {
415            let text = MrkdwnText {
416                r#type: std::marker::PhantomData,
417                text: Some("*Hello* _World_ :smile:".into()),
418                emoji: None,
419                verbatim: Some(false),
420            };
421
422            let expected = serde_json::json!({
423                "type": "mrkdwn",
424                "text": "*Hello* _World_ :smile:",
425                "verbatim": false
426            });
427            let json = serde_json::to_value(&text).unwrap();
428            assert_eq!(json, expected);
429        }
430
431        #[test]
432        fn it_equals_another_mrkdwn_text() {
433            let text1 = MrkdwnText {
434                r#type: std::marker::PhantomData,
435                text: Some("*Hello*".into()),
436                emoji: None,
437                verbatim: Some(true),
438            };
439            let text2 = MrkdwnText {
440                r#type: std::marker::PhantomData,
441                text: Some("*Hello*".into()),
442                emoji: None,
443                verbatim: Some(true),
444            };
445            let text3 = MrkdwnText {
446                r#type: std::marker::PhantomData,
447                text: Some("*Hello*".into()),
448                emoji: None,
449                verbatim: Some(false),
450            };
451            let text4 = MrkdwnText {
452                r#type: std::marker::PhantomData,
453                text: Some("_World_".into()),
454                emoji: None,
455                verbatim: Some(true),
456            };
457            let text5 = MrkdwnText {
458                r#type: std::marker::PhantomData,
459                text: Some("*Hello*".into()),
460                emoji: None,
461                verbatim: None,
462            };
463            assert_eq!(text1, text2);
464            assert_ne!(text1, text3);
465            assert_ne!(text1, text4);
466            assert_eq!(text3, text5);
467        }
468    }
469
470    mod text {
471        use super::*;
472
473        #[test]
474        fn it_serializes_plain_text_variant_to_json() {
475            let text = Text::from(PlainText {
476                r#type: std::marker::PhantomData,
477                text: Some("Hello World :smile:".into()),
478                emoji: Some(true),
479                verbatim: None,
480            });
481
482            let expected = serde_json::json!({
483                "type": "plain_text",
484                "text": "Hello World :smile:",
485                "emoji": true
486            });
487            let json = serde_json::to_value(&text).unwrap();
488            assert_eq!(json, expected);
489        }
490
491        #[test]
492        fn it_serializes_mrkdwn_text_variant_to_json() {
493            let text = Text::from(MrkdwnText {
494                r#type: std::marker::PhantomData,
495                text: Some("*Hello* _World_ :smile:".into()),
496                emoji: None,
497                verbatim: Some(false),
498            });
499
500            let expected = serde_json::json!({
501                "type": "mrkdwn",
502                "text": "*Hello* _World_ :smile:",
503                "verbatim": false
504            });
505            let json = serde_json::to_value(&text).unwrap();
506            assert_eq!(json, expected);
507        }
508    }
509}