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()
318            .elements(vec![] as Vec<Card>)
319            .build()
320            .unwrap_err();
321        assert_eq!(err.object(), "Carousel");
322
323        let errors = err.field("elements");
324        assert!(errors.includes(ValidationErrorKind::EmptyArray));
325    }
326
327    #[test]
328    fn it_requires_elements_to_have_at_most_10_items() {
329        let err = Carousel::builder()
330            .elements(vec![
331                card("Card 1"),
332                card("Card 2"),
333                card("Card 3"),
334                card("Card 4"),
335                card("Card 5"),
336                card("Card 6"),
337                card("Card 7"),
338                card("Card 8"),
339                card("Card 9"),
340                card("Card 10"),
341                card("Card 11"),
342            ])
343            .build()
344            .unwrap_err();
345        assert_eq!(err.object(), "Carousel");
346
347        let errors = err.field("elements");
348        assert!(errors.includes(ValidationErrorKind::MaxArraySize(10)));
349    }
350
351    fn card(title: &str) -> Card {
352        Card {
353            block_id: None,
354            icon: None,
355            title: Some(plain_text(title).into()),
356            subtitle: None,
357            hero_image: None,
358            body: None,
359            actions: None,
360        }
361    }
362}