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/// ## `Text<Plain>`
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/// ## `Text<Mrkdwn>`
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::{Text, Plain, Mrkdwn};
40/// # use std::error::Error;
41/// # fn try_main() -> Result<(), Box<dyn Error>> {
42///
43/// // 1. Plain Text Object
44/// let plain_text = Text::<Plain>::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 = Text::<Mrkdwn>::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 Text<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/// Extension trait for Text objects.
96pub trait TextExt {
97    fn text(&self) -> Option<&str>;
98}
99
100impl<T> TextExt for Text<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/// Text object of type "mrkdwn".
112#[derive(Debug, Clone, Copy, PartialEq)]
113pub struct Mrkdwn;
114
115impl TextBuilder<Plain> {
116    /// get emoji field value.
117    pub fn get_emoji(&self) -> Option<bool> {
118        self.emoji.inner_ref().copied()
119    }
120
121    /// set emoji field value.
122    pub fn set_emoji(self, emoji: Option<impl Into<bool>>) -> TextBuilder<Plain> {
123        Self {
124            emoji: Self::new_emoji(emoji.map(|v| v.into())),
125            ..self
126        }
127    }
128
129    /// set emoji field value.
130    pub fn emoji(self, emoji: impl Into<bool>) -> TextBuilder<Plain> {
131        self.set_emoji(Some(emoji))
132    }
133}
134
135impl TextBuilder<Mrkdwn> {
136    /// get verbatim field value.
137    pub fn get_verbatim(&self) -> Option<bool> {
138        self.verbatim.inner_ref().copied()
139    }
140
141    /// set verbatim field value.
142    pub fn set_verbatim(self, verbatim: Option<impl Into<bool>>) -> TextBuilder<Mrkdwn> {
143        Self {
144            verbatim: Self::new_verbatim(verbatim.map(|v| v.into())),
145            ..self
146        }
147    }
148
149    /// set verbatim field value.
150    pub fn verbatim(self, verbatim: impl Into<bool>) -> TextBuilder<Mrkdwn> {
151        self.set_verbatim(Some(verbatim))
152    }
153}
154
155impl PartialEq for Text<Plain> {
156    fn eq(&self, other: &Self) -> bool {
157        match (self.text(), other.text()) {
158            (Some(text1), Some(text2)) if text1 != text2 => false,
159            (None, Some(_)) | (Some(_), None) => false,
160            _ => self.emoji.unwrap_or(false) == other.emoji.unwrap_or(false),
161        }
162    }
163}
164
165impl PartialEq for Text<Mrkdwn> {
166    fn eq(&self, other: &Self) -> bool {
167        match (self.text(), other.text()) {
168            (Some(text1), Some(text2)) if text1 != text2 => false,
169            (None, Some(_)) | (Some(_), None) => false,
170            _ => self.verbatim.unwrap_or(false) == other.verbatim.unwrap_or(false),
171        }
172    }
173}
174
175impl Serialize for Text<Plain> {
176    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: Serializer,
179    {
180        use serde::ser::SerializeStruct;
181
182        let mut state = serializer.serialize_struct("Text", 3)?;
183
184        state.serialize_field("type", "plain_text")?;
185
186        if let Some(text) = &self.text {
187            state.serialize_field("text", text)?;
188        }
189
190        if let Some(emoji) = &self.emoji {
191            state.serialize_field("emoji", emoji)?;
192        }
193
194        state.end()
195    }
196}
197
198impl Serialize for Text<Mrkdwn> {
199    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
200    where
201        S: Serializer,
202    {
203        use serde::ser::SerializeStruct;
204
205        let mut state = serializer.serialize_struct("Text", 3)?;
206
207        state.serialize_field("type", "mrkdwn")?;
208
209        if let Some(text) = &self.text {
210            state.serialize_field("text", text)?;
211        }
212
213        if let Some(verbatim) = &self.verbatim {
214            state.serialize_field("verbatim", verbatim)?;
215        }
216
217        state.end()
218    }
219}
220
221/// Enum representation of Text objects.
222/// Use this when you need to handle both [`Plain`] and [`Mrkdwn`] text objects.
223#[derive(Debug, Clone, Serialize, PartialEq)]
224#[serde(untagged)]
225pub enum TextContent {
226    /// Plain text object.
227    Plain(Text<Plain>),
228    /// Markdown text object.
229    Mrkdwn(Text<Mrkdwn>),
230}
231
232impl TextExt for TextContent {
233    /// get text field value.
234    fn text(&self) -> Option<&str> {
235        match self {
236            TextContent::Plain(t) => t.text(),
237            TextContent::Mrkdwn(t) => t.text(),
238        }
239    }
240}
241
242impl From<Text<Plain>> for TextContent {
243    fn from(text: Text<Plain>) -> Self {
244        TextContent::Plain(text)
245    }
246}
247
248impl From<Text<Mrkdwn>> for TextContent {
249    fn from(text: Text<Mrkdwn>) -> Self {
250        TextContent::Mrkdwn(text)
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::errors::*;
258
259    #[test]
260    fn it_requires_text() {
261        let err = Text::<Plain>::builder().build().unwrap_err();
262        assert_eq!(err.object(), "Text");
263        let text_err = err.field("text");
264        assert!(text_err.includes(ValidationErrorKind::Required));
265    }
266
267    #[test]
268    fn it_requires_text_more_than_1_character() {
269        let err = Text::<Plain>::builder().text("").build().unwrap_err();
270        assert_eq!(err.object(), "Text");
271        let text_err = err.field("text");
272        assert!(text_err.includes(ValidationErrorKind::MinTextLength(1)));
273    }
274
275    #[test]
276    fn it_requires_text_less_than_3000_characters() {
277        let err = Text::<Plain>::builder()
278            .text("a".repeat(3001))
279            .build()
280            .unwrap_err();
281        assert_eq!(err.object(), "Text");
282        let text_err = err.field("text");
283        assert!(text_err.includes(ValidationErrorKind::MaxTextLength(3000)));
284    }
285
286    mod plain_text {
287        use super::*;
288
289        #[test]
290        fn it_implements_builder() {
291            let expected = Text::<Plain> {
292                r#type: std::marker::PhantomData,
293                text: Some("Hello World:smile:".into()),
294                emoji: Some(true),
295                verbatim: None,
296            };
297
298            let text = Text::builder()
299                .text("Hello World:smile:")
300                .emoji(true)
301                .build()
302                .unwrap();
303
304            assert_eq!(text, expected);
305
306            let text = Text::builder()
307                .set_text(Some("Hello World:smile:"))
308                .set_emoji(Some(true))
309                .build()
310                .unwrap();
311
312            assert_eq!(text, expected);
313        }
314
315        #[test]
316        fn it_serializes_to_json() {
317            let text = Text::<Plain> {
318                r#type: std::marker::PhantomData,
319                text: Some("Hello World :smile:".into()),
320                emoji: Some(true),
321                verbatim: None,
322            };
323
324            let expected = serde_json::json!({
325                "type": "plain_text",
326                "text": "Hello World :smile:",
327                "emoji": true
328            });
329            let json = serde_json::to_value(&text).unwrap();
330            assert_eq!(json, expected);
331        }
332
333        #[test]
334        fn it_equals_another_plain_text() {
335            let text1 = Text::<Plain> {
336                r#type: std::marker::PhantomData,
337                text: Some("Hello".into()),
338                emoji: Some(true),
339                verbatim: None,
340            };
341            let text2 = Text::<Plain> {
342                r#type: std::marker::PhantomData,
343                text: Some("Hello".into()),
344                emoji: Some(true),
345                verbatim: None,
346            };
347            let text3 = Text::<Plain> {
348                r#type: std::marker::PhantomData,
349                text: Some("Hello".into()),
350                emoji: Some(false),
351                verbatim: None,
352            };
353            let text4 = Text::<Plain> {
354                r#type: std::marker::PhantomData,
355                text: Some("World".into()),
356                emoji: Some(true),
357                verbatim: None,
358            };
359            let text5 = Text::<Plain> {
360                r#type: std::marker::PhantomData,
361                text: Some("Hello".into()),
362                emoji: None,
363                verbatim: None,
364            };
365            assert_eq!(text1, text2);
366            assert_ne!(text1, text3);
367            assert_ne!(text1, text4);
368            assert_eq!(text3, text5);
369        }
370    }
371
372    mod mrkdwn_text {
373        use super::*;
374
375        #[test]
376        fn it_implements_builder() {
377            let expected = Text::<Mrkdwn> {
378                r#type: std::marker::PhantomData,
379                text: Some("*Hello* _World_ :smile:".into()),
380                emoji: None,
381                verbatim: Some(false),
382            };
383
384            let text = Text::builder()
385                .text("*Hello* _World_ :smile:")
386                .verbatim(false)
387                .build()
388                .unwrap();
389
390            assert_eq!(text, expected);
391
392            let text = Text::builder()
393                .set_text(Some("*Hello* _World_ :smile:"))
394                .set_verbatim(Some(false))
395                .build()
396                .unwrap();
397
398            assert_eq!(text, expected);
399        }
400
401        #[test]
402        fn it_serializes_to_json() {
403            let text = Text::<Mrkdwn> {
404                r#type: std::marker::PhantomData,
405                text: Some("*Hello* _World_ :smile:".into()),
406                emoji: None,
407                verbatim: Some(false),
408            };
409
410            let expected = serde_json::json!({
411                "type": "mrkdwn",
412                "text": "*Hello* _World_ :smile:",
413                "verbatim": false
414            });
415            let json = serde_json::to_value(&text).unwrap();
416            assert_eq!(json, expected);
417        }
418
419        #[test]
420        fn it_equals_another_mrkdwn_text() {
421            let text1 = Text::<Mrkdwn> {
422                r#type: std::marker::PhantomData,
423                text: Some("*Hello*".into()),
424                emoji: None,
425                verbatim: Some(true),
426            };
427            let text2 = Text::<Mrkdwn> {
428                r#type: std::marker::PhantomData,
429                text: Some("*Hello*".into()),
430                emoji: None,
431                verbatim: Some(true),
432            };
433            let text3 = Text::<Mrkdwn> {
434                r#type: std::marker::PhantomData,
435                text: Some("*Hello*".into()),
436                emoji: None,
437                verbatim: Some(false),
438            };
439            let text4 = Text::<Mrkdwn> {
440                r#type: std::marker::PhantomData,
441                text: Some("_World_".into()),
442                emoji: None,
443                verbatim: Some(true),
444            };
445            let text5 = Text::<Mrkdwn> {
446                r#type: std::marker::PhantomData,
447                text: Some("*Hello*".into()),
448                emoji: None,
449                verbatim: None,
450            };
451            assert_eq!(text1, text2);
452            assert_ne!(text1, text3);
453            assert_ne!(text1, text4);
454            assert_eq!(text3, text5);
455        }
456    }
457
458    mod text_content {
459        use super::*;
460
461        #[test]
462        fn it_serializes_plain_text_variant_to_json() {
463            let text = TextContent::from(Text::<Plain> {
464                r#type: std::marker::PhantomData,
465                text: Some("Hello World :smile:".into()),
466                emoji: Some(true),
467                verbatim: None,
468            });
469
470            let expected = serde_json::json!({
471                "type": "plain_text",
472                "text": "Hello World :smile:",
473                "emoji": true
474            });
475            let json = serde_json::to_value(&text).unwrap();
476            assert_eq!(json, expected);
477        }
478
479        #[test]
480        fn it_serializes_mrkdwn_text_variant_to_json() {
481            let text = TextContent::from(Text::<Mrkdwn> {
482                r#type: std::marker::PhantomData,
483                text: Some("*Hello* _World_ :smile:".into()),
484                emoji: None,
485                verbatim: Some(false),
486            });
487
488            let expected = serde_json::json!({
489                "type": "mrkdwn",
490                "text": "*Hello* _World_ :smile:",
491                "verbatim": false
492            });
493            let json = serde_json::to_value(&text).unwrap();
494            assert_eq!(json, expected);
495        }
496    }
497}