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}