slack_messaging/blocks/
card.rs1use crate::blocks::elements::{Button, Image};
2use crate::composition_objects::TextContent;
3use crate::errors::ValidationErrorKind;
4use crate::validators::*;
5
6use serde::Serialize;
7use slack_messaging_derive::Builder;
8
9#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
108#[builder(validate = "validate")]
109#[serde(tag = "type", rename = "card")]
110pub struct Card {
111 #[serde(skip_serializing_if = "Option::is_none")]
112 #[builder(validate("text::max_255"))]
113 pub(crate) block_id: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub(crate) hero_image: Option<Image>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub(crate) icon: Option<Image>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
122 #[builder(validate("text_object::max_150"))]
123 pub(crate) title: Option<TextContent>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
126 #[builder(validate("text_object::max_150"))]
127 pub(crate) subtitle: Option<TextContent>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
130 #[builder(validate("text_object::max_200"))]
131 pub(crate) body: Option<TextContent>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
134 #[builder(push_item = "action")]
135 pub(crate) actions: Option<Vec<Button>>,
136}
137
138fn validate(val: &Card) -> Vec<ValidationErrorKind> {
139 if val.hero_image.is_none()
140 && val.title.is_none()
141 && val
142 .actions
143 .as_ref()
144 .is_none_or(|actions| actions.is_empty())
145 && val.body.is_none()
146 {
147 vec![ValidationErrorKind::AtLeastOneOf4(
148 "hero_image",
149 "title",
150 "actions",
151 "body",
152 )]
153 } else {
154 vec![]
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::composition_objects::test_helpers::*;
162 use crate::errors::*;
163
164 #[test]
165 fn it_implements_builder() {
166 let expected = Card {
167 block_id: Some("card1".to_string()),
168 hero_image: Some(Image {
169 image_url: Some("https://picsum.photos/400/300".into()),
170 alt_text: Some("Sample hero image".into()),
171 slack_file: None,
172 }),
173 icon: Some(Image {
174 image_url: Some("https://picsum.photos/36/36".into()),
175 alt_text: Some("Icon".into()),
176 slack_file: None,
177 }),
178 title: Some(plain_text("Lumon Industries").into()),
179 subtitle: Some(plain_text("Committed to work-life balance").into()),
180 body: Some(plain_text("Please enjoy each card equally.").into()),
181 actions: Some(vec![Button {
182 text: Some(plain_text("Action Button")),
183 action_id: Some("button_action".into()),
184 url: None,
185 value: None,
186 style: None,
187 confirm: None,
188 accessibility_label: None,
189 }]),
190 };
191
192 let val = Card::builder()
193 .set_block_id(Some("card1"))
194 .set_hero_image(Some(
195 Image::builder()
196 .image_url("https://picsum.photos/400/300")
197 .alt_text("Sample hero image")
198 .build()
199 .unwrap(),
200 ))
201 .set_icon(Some(
202 Image::builder()
203 .image_url("https://picsum.photos/36/36")
204 .alt_text("Icon")
205 .build()
206 .unwrap(),
207 ))
208 .set_title(Some(plain_text("Lumon Industries")))
209 .set_subtitle(Some(plain_text("Committed to work-life balance")))
210 .set_body(Some(plain_text("Please enjoy each card equally.")))
211 .set_actions(Some(vec![
212 Button::builder()
213 .text(plain_text("Action Button"))
214 .action_id("button_action")
215 .build()
216 .unwrap(),
217 ]))
218 .build()
219 .unwrap();
220
221 assert_eq!(val, expected);
222
223 let val = Card::builder()
224 .block_id("card1")
225 .hero_image(
226 Image::builder()
227 .image_url("https://picsum.photos/400/300")
228 .alt_text("Sample hero image")
229 .build()
230 .unwrap(),
231 )
232 .icon(
233 Image::builder()
234 .image_url("https://picsum.photos/36/36")
235 .alt_text("Icon")
236 .build()
237 .unwrap(),
238 )
239 .title(plain_text("Lumon Industries"))
240 .subtitle(plain_text("Committed to work-life balance"))
241 .body(plain_text("Please enjoy each card equally."))
242 .actions(vec![
243 Button::builder()
244 .text(plain_text("Action Button"))
245 .action_id("button_action")
246 .build()
247 .unwrap(),
248 ])
249 .build()
250 .unwrap();
251
252 assert_eq!(val, expected);
253 }
254
255 #[test]
256 fn it_implements_push_item_method() {
257 let expected = Card {
258 block_id: None,
259 hero_image: None,
260 icon: None,
261 title: None,
262 subtitle: None,
263 body: Some(plain_text("Please enjoy each card equally.").into()),
264 actions: Some(vec![Button {
265 text: Some(plain_text("Action Button")),
266 action_id: Some("button_action".into()),
267 url: None,
268 value: None,
269 style: None,
270 confirm: None,
271 accessibility_label: None,
272 }]),
273 };
274
275 let val = Card::builder()
276 .body(plain_text("Please enjoy each card equally."))
277 .action(
278 Button::builder()
279 .text(plain_text("Action Button"))
280 .action_id("button_action")
281 .build()
282 .unwrap(),
283 )
284 .build()
285 .unwrap();
286
287 assert_eq!(val, expected);
288 }
289
290 #[test]
291 fn it_requires_title_less_than_150_characters_long() {
292 let err = Card::builder()
293 .title(plain_text("a".repeat(151)))
294 .build()
295 .unwrap_err();
296 assert_eq!(err.object(), "Card");
297
298 let errors = err.field("title");
299 assert!(errors.includes(ValidationErrorKind::MaxTextLength(150)));
300 }
301
302 #[test]
303 fn it_requires_subtitle_less_than_150_characters_long() {
304 let err = Card::builder()
305 .title(plain_text("Valid Title"))
306 .subtitle(plain_text("a".repeat(151)))
307 .build()
308 .unwrap_err();
309 assert_eq!(err.object(), "Card");
310
311 let errors = err.field("subtitle");
312 assert!(errors.includes(ValidationErrorKind::MaxTextLength(150)));
313 }
314
315 #[test]
316 fn it_requires_body_less_than_200_characters_long() {
317 let err = Card::builder()
318 .body(plain_text("a".repeat(201)))
319 .build()
320 .unwrap_err();
321 assert_eq!(err.object(), "Card");
322
323 let errors = err.field("body");
324 assert!(errors.includes(ValidationErrorKind::MaxTextLength(200)));
325 }
326
327 #[test]
328 fn it_requires_at_least_one_of_hero_image_title_actions_body() {
329 let err = Card::builder().build().unwrap_err();
330 assert_eq!(err.object(), "Card");
331
332 let errors = err.across_fields();
333 assert!(errors.includes(ValidationErrorKind::AtLeastOneOf4(
334 "hero_image",
335 "title",
336 "actions",
337 "body"
338 )));
339
340 let card = Card::builder()
341 .hero_image(
342 Image::builder()
343 .image_url("https://picsum.photos/400/300")
344 .alt_text("Sample hero image")
345 .build()
346 .unwrap(),
347 )
348 .build();
349 assert!(card.is_ok());
350
351 let card = Card::builder()
352 .title(plain_text("Lumon Industries"))
353 .build();
354 assert!(card.is_ok());
355
356 let card = Card::builder()
357 .body(plain_text("Please enjoy each card equally."))
358 .build();
359 assert!(card.is_ok());
360
361 let card = Card::builder()
362 .action(
363 Button::builder()
364 .text(plain_text("Action Button"))
365 .action_id("button_action")
366 .build()
367 .unwrap(),
368 )
369 .build();
370 assert!(card.is_ok());
371 }
372
373 #[test]
374 fn it_requires_block_id_less_than_255_characters_long() {
375 let err = Card::builder()
376 .block_id("a".repeat(256))
377 .build()
378 .unwrap_err();
379 assert_eq!(err.object(), "Card");
380
381 let errors = err.field("block_id");
382 assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
383 }
384}