Skip to main content

slack_messaging/blocks/
carousel.rs

1use crate::blocks::Card;
2use crate::validators::*;
3
4use serde::Serialize;
5use slack_messaging_derive::Builder;
6
7/// [Carousel block](https://docs.slack.dev/reference/block-kit/blocks/carousel-block)
8/// representation
9///
10/// # Fields and Validations
11///
12/// For more details, see the [official
13/// documentation](https://docs.slack.dev/reference/block-kit/blocks/carousel-block).
14///
15/// | Field | Type | Required | Validation |
16/// |-------|------|----------|------------|
17/// | elements | Vec<[Card]> | Yes | Must contain as least 1 card and at most 10 cards. |
18/// | block_id | String | No | Must be 255 characters or less. |
19///
20/// # Examples
21///
22/// ```
23/// use slack_messaging::{plain_text, mrkdwn};
24/// use slack_messaging::blocks::{Card, Carousel};
25/// use slack_messaging::blocks::elements::{Button, Image};
26/// # use std::error::Error;
27///
28/// # fn try_main() -> Result<(), Box<dyn Error>> {
29/// let carousel = Carousel::builder()
30///     .element(
31///         Card::builder()
32///             .block_id("carousel-card-1")
33///             .icon(
34///                 Image::builder()
35///                     .image_url("https://picsum.photos/36/36")
36///                     .alt_text("Icon")
37///                     .build()?
38///             )
39///             .title(mrkdwn!("MDR")?)
40///             .subtitle(mrkdwn!("Refining data files")?)
41///             .hero_image(
42///                 Image::builder()
43///                     .image_url("https://picsum.photos/400/300")
44///                     .alt_text("Sample hero image")
45///                     .build()?
46///             )
47///             .body(mrkdwn!("Blue badge required to gain access.")?)
48///             .action(
49///                 Button::builder()
50///                     .text(plain_text!("Action Button")?)
51///                     .action_id("button_action_1")
52///                     .build()?
53///             )
54///             .build()?
55///     )
56///     .element(
57///         Card::builder()
58///             .block_id("carousel-card-2")
59///             .icon(
60///                 Image::builder()
61///                     .image_url("https://picsum.photos/36/36")
62///                     .alt_text("Icon")
63///                     .build()?
64///             )
65///             .title(mrkdwn!("O&D")?)
66///             .subtitle(mrkdwn!("Storage, maintenance, and rotation of art pieces")?)
67///             .hero_image(
68///                 Image::builder()
69///                     .image_url("https://picsum.photos/400/300")
70///                     .alt_text("Sample hero image")
71///                     .build()?
72///             )
73///             .body(mrkdwn!("Green badge required to gain access.")?)
74///             .action(
75///                 Button::builder()
76///                     .text(plain_text!("Action Button")?)
77///                     .action_id("button_action_2")
78///                     .build()?
79///             )
80///             .build()?
81///     )
82///     .element(
83///         Card::builder()
84///             .block_id("carousel-card-3")
85///             .icon(
86///                 Image::builder()
87///                     .image_url("https://picsum.photos/36/36")
88///                     .alt_text("Icon")
89///                     .build()?
90///             )
91///             .title(mrkdwn!("Wellness Center")?)
92///             .subtitle(mrkdwn!("Wellness sessions")?)
93///             .hero_image(
94///                 Image::builder()
95///                     .image_url("https://picsum.photos/400/300")
96///                     .alt_text("Sample hero image")
97///                     .build()?
98///             )
99///             .body(mrkdwn!("Please take a seat in the waiting room until called.")?)
100///             .action(
101///                 Button::builder()
102///                     .text(plain_text!("Action Button")?)
103///                     .action_id("button_action_3")
104///                     .build()?
105///             )
106///             .build()?
107///     )
108///     .build()?;
109///
110/// let expected = serde_json::json!({
111///     "type": "carousel",
112///     "elements": [
113///         {
114///             "type": "card",
115///             "block_id": "carousel-card-1",
116///             "icon": {
117///                 "type": "image",
118///                 "image_url": "https://picsum.photos/36/36",
119///                 "alt_text": "Icon"
120///             },
121///             "title": {
122///                 "type": "mrkdwn",
123///                 "text": "MDR"
124///             },
125///             "subtitle": {
126///                 "type": "mrkdwn",
127///                 "text": "Refining data files"
128///             },
129///             "hero_image": {
130///                 "type": "image",
131///                 "image_url": "https://picsum.photos/400/300",
132///                 "alt_text": "Sample hero image"
133///             },
134///             "body": {
135///                 "type": "mrkdwn",
136///                 "text": "Blue badge required to gain access."
137///             },
138///             "actions": [
139///                 {
140///                     "type": "button",
141///                     "text": {
142///                         "type": "plain_text",
143///                         "text": "Action Button"
144///                     },
145///                     "action_id": "button_action_1"
146///                 }
147///             ]
148///         },
149///         {
150///             "type": "card",
151///             "block_id": "carousel-card-2",
152///             "icon": {
153///                 "type": "image",
154///                 "image_url": "https://picsum.photos/36/36",
155///                 "alt_text": "Icon"
156///             },
157///             "title": {
158///                 "type": "mrkdwn",
159///                 "text": "O&D"
160///             },
161///             "subtitle": {
162///                 "type": "mrkdwn",
163///                 "text": "Storage, maintenance, and rotation of art pieces"
164///             },
165///             "hero_image": {
166///                 "type": "image",
167///                 "image_url": "https://picsum.photos/400/300",
168///                 "alt_text": "Sample hero image"
169///             },
170///             "body": {
171///                 "type": "mrkdwn",
172///                 "text": "Green badge required to gain access."
173///             },
174///             "actions": [
175///                 {
176///                     "type": "button",
177///                     "text": {
178///                         "type": "plain_text",
179///                         "text": "Action Button"
180///                     },
181///                     "action_id": "button_action_2"
182///                 }
183///             ]
184///         },
185///         {
186///             "type": "card",
187///             "block_id": "carousel-card-3",
188///             "icon": {
189///                 "type": "image",
190///                 "image_url": "https://picsum.photos/36/36",
191///                 "alt_text": "Icon"
192///             },
193///             "title": {
194///                 "type": "mrkdwn",
195///                 "text": "Wellness Center"
196///             },
197///             "subtitle": {
198///                 "type": "mrkdwn",
199///                 "text": "Wellness sessions"
200///             },
201///             "hero_image": {
202///                 "type": "image",
203///                 "image_url": "https://picsum.photos/400/300",
204///                 "alt_text": "Sample hero image"
205///             },
206///             "body": {
207///                 "type": "mrkdwn",
208///                 "text": "Please take a seat in the waiting room until called."
209///             },
210///             "actions": [
211///                 {
212///                     "type": "button",
213///                     "text": {
214///                         "type": "plain_text",
215///                         "text": "Action Button"
216///                     },
217///                     "action_id": "button_action_3"
218///                 }
219///             ]
220///         }
221///     ]
222/// });
223///
224/// let json = serde_json::to_value(carousel).unwrap();
225///
226/// assert_eq!(json, expected);
227/// #     Ok(())
228/// # }
229/// # fn main() {
230/// #     try_main().unwrap()
231/// # }
232/// ```
233#[derive(Debug, Clone, Serialize, Builder, PartialEq)]
234#[serde(tag = "type", rename = "carousel")]
235pub struct Carousel {
236    #[serde(skip_serializing_if = "Option::is_none")]
237    #[builder(validate("text::max_255"))]
238    pub(crate) block_id: Option<String>,
239
240    #[builder(
241        push_item = "element",
242        validate("required", "list::not_empty", "list::max_item_10")
243    )]
244    pub(crate) elements: Option<Vec<Card>>,
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::composition_objects::test_helpers::*;
251    use crate::errors::*;
252
253    #[test]
254    fn it_implements_builder() {
255        let expected = Carousel {
256            block_id: Some("carousel_0".into()),
257            elements: Some(vec![card("Card 1"), card("Card 2")]),
258        };
259
260        let val = Carousel::builder()
261            .set_block_id(Some("carousel_0"))
262            .set_elements(Some(vec![card("Card 1"), card("Card 2")]))
263            .build()
264            .unwrap();
265
266        assert_eq!(val, expected);
267
268        let val = Carousel::builder()
269            .block_id("carousel_0")
270            .elements(vec![card("Card 1"), card("Card 2")])
271            .build()
272            .unwrap();
273
274        assert_eq!(val, expected);
275    }
276
277    #[test]
278    fn it_implements_push_item_method() {
279        let expected = Carousel {
280            block_id: None,
281            elements: Some(vec![card("Card 1"), card("Card 2")]),
282        };
283
284        let val = Carousel::builder()
285            .element(card("Card 1"))
286            .element(card("Card 2"))
287            .build()
288            .unwrap();
289
290        assert_eq!(val, expected);
291    }
292
293    #[test]
294    fn it_requires_block_id_less_than_255_characters_long() {
295        let err = Carousel::builder()
296            .block_id("a".repeat(256))
297            .element(card("Card 1"))
298            .build()
299            .unwrap_err();
300        assert_eq!(err.object(), "Carousel");
301
302        let errors = err.field("block_id");
303        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
304    }
305
306    #[test]
307    fn it_requires_elements_field() {
308        let err = Carousel::builder().build().unwrap_err();
309        assert_eq!(err.object(), "Carousel");
310
311        let errors = err.field("elements");
312        assert!(errors.includes(ValidationErrorKind::Required));
313    }
314
315    #[test]
316    fn it_requires_elements_to_have_at_least_1_item() {
317        let err = Carousel::builder().elements(vec![]).build().unwrap_err();
318        assert_eq!(err.object(), "Carousel");
319
320        let errors = err.field("elements");
321        assert!(errors.includes(ValidationErrorKind::EmptyArray));
322    }
323
324    #[test]
325    fn it_requires_elements_to_have_at_most_10_items() {
326        let err = Carousel::builder()
327            .elements(vec![
328                card("Card 1"),
329                card("Card 2"),
330                card("Card 3"),
331                card("Card 4"),
332                card("Card 5"),
333                card("Card 6"),
334                card("Card 7"),
335                card("Card 8"),
336                card("Card 9"),
337                card("Card 10"),
338                card("Card 11"),
339            ])
340            .build()
341            .unwrap_err();
342        assert_eq!(err.object(), "Carousel");
343
344        let errors = err.field("elements");
345        assert!(errors.includes(ValidationErrorKind::MaxArraySize(10)));
346    }
347
348    fn card(title: &str) -> Card {
349        Card {
350            block_id: None,
351            icon: None,
352            title: Some(plain_text(title).into()),
353            subtitle: None,
354            hero_image: None,
355            body: None,
356            actions: None,
357        }
358    }
359}