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}