slack_messaging/blocks/data_table/mod.rs
1use crate::errors::ValidationErrorKind;
2use crate::value::Value;
3use crate::validators::*;
4
5use serde::Serialize;
6use slack_messaging_derive::Builder;
7
8/// Builders for [`DataTable`] related objects.
9pub mod builders;
10
11mod cell;
12mod row;
13
14pub use crate::blocks::table::RawText;
15pub use cell::{DataTableCell, RawNumber};
16pub use row::DataTableRow;
17
18/// [Data Table block](https://docs.slack.dev/reference/block-kit/blocks/data-table-block)
19/// representation.
20///
21/// # Fields and Validations
22///
23/// For more details, see the [official
24/// documentation](https://docs.slack.dev/reference/block-kit/blocks/data-table-block).
25///
26/// | Field | Type | Required | Validation |
27/// |-------|------|----------|------------|
28/// | block_id | String | No | Maximum 255 characters. |
29/// | rows | Vec<[DataTableRow]> | Yes | Maximum 101 items (100 regular rows plus the header). Minimum 2 items (1 regular row plus the header). The first row is a header, and `rich_text` cannot be used for the header cells. |
30/// | caption | String | Yes | N/A |
31/// | page_size | i64 | No | Minimum 1, maximum 100. |
32/// | row_header_column_index | i64 | No | Minimum 0. |
33///
34/// # Example
35///
36/// ```
37/// use slack_messaging::blocks::rich_text::prelude::*;
38/// use slack_messaging::blocks::data_table::*;
39/// # use std::error::Error;
40///
41/// # fn try_main() -> Result<(), Box<dyn Error>> {
42/// let table = DataTable::builder()
43/// .caption("A Fabulous Table")
44/// .row(
45/// DataTableRow::builder()
46/// .cell("Name")
47/// .cell("Department")
48/// .cell("Badge")
49/// .build()?
50/// )
51/// .row(
52/// DataTableRow::builder()
53/// .cell("Data Refinement Department")
54/// .cell("MDR")
55/// .cell(
56/// RichText::builder()
57/// .element(
58/// RichTextSection::builder()
59/// .element(
60/// RichTextElementText::builder()
61/// .text("Blue")
62/// .style(
63/// RichTextStyle::builder()
64/// .bold(true)
65/// .build()?
66/// )
67/// .build()?
68/// )
69/// .build()?
70/// )
71/// .build()?
72/// )
73/// .build()?
74/// )
75/// .row(
76/// DataTableRow::builder()
77/// .cell("Art Sourcing Department")
78/// .cell("O&D")
79/// .cell(
80/// RichText::builder()
81/// .element(
82/// RichTextSection::builder()
83/// .element(
84/// RichTextElementText::builder()
85/// .text("Green")
86/// .build()?
87/// )
88/// .element(
89/// RichTextElementText::builder()
90/// .text("review")
91/// .style(
92/// RichTextStyle::builder()
93/// .italic(true)
94/// .build()?
95/// )
96/// .build()?
97/// )
98/// .build()?
99/// )
100/// .build()?
101/// )
102/// .build()?
103/// )
104/// .row(
105/// DataTableRow::builder()
106/// .cell("Wellness Department")
107/// .cell("Wellness Center")
108/// .cell(
109/// RichText::builder()
110/// .element(
111/// RichTextSection::builder()
112/// .element(
113/// RichTextElementText::builder()
114/// .text("Limited")
115/// .style(
116/// RichTextStyle::builder()
117/// .bold(true)
118/// .build()?
119/// )
120/// .build()?
121/// )
122/// .build()?
123/// )
124/// .build()?
125/// )
126/// .build()?
127/// )
128/// .build()?;
129///
130/// let expected = serde_json::json!({
131/// "type": "data_table",
132/// "caption": "A Fabulous Table",
133/// "rows": [
134/// [
135/// {
136/// "type": "raw_text",
137/// "text": "Name"
138/// },
139/// {
140/// "type": "raw_text",
141/// "text": "Department"
142/// },
143/// {
144/// "type": "raw_text",
145/// "text": "Badge"
146/// }
147/// ],
148/// [
149/// {
150/// "type": "raw_text",
151/// "text": "Data Refinement Department"
152/// },
153/// {
154/// "type": "raw_text",
155/// "text": "MDR"
156/// },
157/// {
158/// "type": "rich_text",
159/// "elements": [
160/// {
161/// "type": "rich_text_section",
162/// "elements": [
163/// {
164/// "type": "text",
165/// "text": "Blue",
166/// "style": {
167/// "bold": true
168/// }
169/// }
170/// ]
171/// }
172/// ]
173/// }
174/// ],
175/// [
176/// {
177/// "type": "raw_text",
178/// "text": "Art Sourcing Department"
179/// },
180/// {
181/// "type": "raw_text",
182/// "text": "O&D"
183/// },
184/// {
185/// "type": "rich_text",
186/// "elements": [
187/// {
188/// "type": "rich_text_section",
189/// "elements": [
190/// {
191/// "type": "text",
192/// "text": "Green"
193/// },
194/// {
195/// "type": "text",
196/// "text": "review",
197/// "style": {
198/// "italic": true
199/// }
200/// }
201/// ]
202/// }
203/// ]
204/// }
205/// ],
206/// [
207/// {
208/// "type": "raw_text",
209/// "text": "Wellness Department"
210/// },
211/// {
212/// "type": "raw_text",
213/// "text": "Wellness Center"
214/// },
215/// {
216/// "type": "rich_text",
217/// "elements": [
218/// {
219/// "type": "rich_text_section",
220/// "elements": [
221/// {
222/// "type": "text",
223/// "text": "Limited",
224/// "style": {
225/// "bold": true
226/// }
227/// }
228/// ]
229/// }
230/// ]
231/// }
232/// ]
233/// ]
234/// });
235///
236/// let json = serde_json::to_value(table).unwrap();
237///
238/// assert_eq!(json, expected);
239/// # Ok(())
240/// # }
241/// # fn main() {
242/// # try_main().unwrap()
243/// # }
244/// ```
245#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
246#[serde(tag = "type", rename = "data_table")]
247pub struct DataTable {
248 #[serde(skip_serializing_if = "Option::is_none")]
249 #[builder(validate("text::max_255"))]
250 pub(crate) block_id: Option<String>,
251
252 #[builder(
253 push_item = "row",
254 validate("required", "list::min_item_2", "list::max_item_101", "valid_header")
255 )]
256 pub(crate) rows: Option<Vec<DataTableRow>>,
257
258 #[builder(validate("required"))]
259 pub(crate) caption: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
262 #[builder(validate("integer::min_1", "integer::max_100"))]
263 pub(crate) page_size: Option<i64>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
266 #[builder(validate("integer::min_0"))]
267 pub(crate) row_header_column_index: Option<i64>,
268}
269
270fn valid_header(value: Value<Vec<DataTableRow>>) -> Value<Vec<DataTableRow>> {
271 list::inner_validator(value, ValidationErrorKind::RichTextTableHeader, |rows| {
272 rows.first().and_then(|row| row.cells.as_ref()).is_some_and(|cells| {
273 cells.iter().any(DataTableCell::is_rich_text)
274 })
275 })
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::errors::*;
282 use crate::blocks::rich_text::prelude::*;
283
284 #[test]
285 fn it_implements_builder() {
286 let expected = DataTable {
287 block_id: Some("table_0".into()),
288 caption: Some("A Fabulous Table".into()),
289 rows: Some(vec![
290 row(vec!["foo", "bar", "baz"]),
291 row(vec!["000", "001", "002"]),
292 ]),
293 page_size: Some(10),
294 row_header_column_index: Some(0),
295 };
296
297 let val = DataTable::builder()
298 .set_block_id(Some("table_0"))
299 .set_caption(Some("A Fabulous Table"))
300 .set_rows(Some(vec![
301 row(vec!["foo", "bar", "baz"]),
302 row(vec!["000", "001", "002"]),
303 ]))
304 .set_page_size(Some(10))
305 .set_row_header_column_index(Some(0))
306 .build()
307 .unwrap();
308
309 assert_eq!(val, expected);
310
311 let val = DataTable::builder()
312 .block_id("table_0")
313 .caption("A Fabulous Table")
314 .rows(vec![
315 row(vec!["foo", "bar", "baz"]),
316 row(vec!["000", "001", "002"]),
317 ])
318 .page_size(10)
319 .row_header_column_index(0)
320 .build()
321 .unwrap();
322
323 assert_eq!(val, expected);
324 }
325
326 #[test]
327 fn it_implements_push_item_method_for_rows_field() {
328 let expected = DataTable {
329 block_id: None,
330 caption: Some("A Fabulous Table".into()),
331 rows: Some(vec![
332 row(vec!["foo", "bar", "baz"]),
333 row(vec!["000", "001", "002"]),
334 ]),
335 page_size: None,
336 row_header_column_index: None,
337 };
338
339 let val = DataTable::builder()
340 .caption("A Fabulous Table")
341 .row(row(vec!["foo", "bar", "baz"]))
342 .row(row(vec!["000", "001", "002"]))
343 .build()
344 .unwrap();
345
346 assert_eq!(val, expected);
347 }
348
349 #[test]
350 fn it_requires_block_id_to_be_max_255_characters() {
351 let err = DataTable::builder()
352 .block_id("a".repeat(256))
353 .caption("A Fabulous Table")
354 .row(row(vec!["foo", "bar", "baz"]))
355 .row(row(vec!["000", "001", "002"]))
356 .build()
357 .unwrap_err();
358 assert_eq!(err.object(), "DataTable");
359
360 let errors = err.field("block_id");
361 assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
362 }
363
364 #[test]
365 fn it_requires_rows_field() {
366 let err = DataTable::builder()
367 .caption("A Fabulous Table")
368 .build()
369 .unwrap_err();
370 assert_eq!(err.object(), "DataTable");
371
372 let errors = err.field("rows");
373 assert!(errors.includes(ValidationErrorKind::Required));
374 }
375
376 #[test]
377 fn it_requires_rows_field_to_have_at_least_2_items() {
378 let err = DataTable::builder()
379 .caption("A Fabulous Table")
380 .row(row(vec!["foo", "bar", "baz"]))
381 .build()
382 .unwrap_err();
383 assert_eq!(err.object(), "DataTable");
384
385 let errors = err.field("rows");
386 assert!(errors.includes(ValidationErrorKind::MinArraySize(2)));
387 }
388
389 #[test]
390 fn it_requires_rows_field_to_have_at_most_101_items() {
391 let rows: Vec<DataTableRow> = (0..102).map(|_| row(vec!["foo", "bar", "baz"])).collect();
392 let err = DataTable::builder().rows(rows).build().unwrap_err();
393 assert_eq!(err.object(), "DataTable");
394
395 let errors = err.field("rows");
396 assert!(errors.includes(ValidationErrorKind::MaxArraySize(101)));
397 }
398
399 #[test]
400 fn it_requires_rows_field_to_have_header_row_without_rich_text() {
401 let err = DataTable::builder()
402 .caption("A Fabulous Table")
403 .row(rich_text_row(vec!["foo", "bar", "baz"]))
404 .row(row(vec!["000", "001", "002"]))
405 .build()
406 .unwrap_err();
407 assert_eq!(err.object(), "DataTable");
408
409 let errors = err.field("rows");
410 assert!(errors.includes(ValidationErrorKind::RichTextTableHeader));
411 }
412
413 #[test]
414 fn it_requires_caption_field() {
415 let err = DataTable::builder()
416 .row(row(vec!["foo", "bar", "baz"]))
417 .row(row(vec!["000", "001", "002"]))
418 .build()
419 .unwrap_err();
420 assert_eq!(err.object(), "DataTable");
421
422 let errors = err.field("caption");
423 assert!(errors.includes(ValidationErrorKind::Required));
424 }
425
426 #[test]
427 fn it_requires_page_size_to_be_at_least_1() {
428 let err = DataTable::builder()
429 .caption("A Fabulous Table")
430 .row(row(vec!["foo", "bar", "baz"]))
431 .row(row(vec!["000", "001", "002"]))
432 .page_size(0)
433 .build()
434 .unwrap_err();
435 assert_eq!(err.object(), "DataTable");
436
437 let errors = err.field("page_size");
438 assert!(errors.includes(ValidationErrorKind::MinIntegerValue(1)));
439 }
440
441 #[test]
442 fn it_requires_page_size_to_be_at_most_100() {
443 let err = DataTable::builder()
444 .caption("A Fabulous Table")
445 .row(row(vec!["foo", "bar", "baz"]))
446 .row(row(vec!["000", "001", "002"]))
447 .page_size(101)
448 .build()
449 .unwrap_err();
450 assert_eq!(err.object(), "DataTable");
451
452 let errors = err.field("page_size");
453 assert!(errors.includes(ValidationErrorKind::MaxIntegerValue(100)));
454 }
455
456 #[test]
457 fn it_requires_row_header_column_index_to_be_at_least_0() {
458 let err = DataTable::builder()
459 .caption("A Fabulous Table")
460 .row(row(vec!["foo", "bar", "baz"]))
461 .row(row(vec!["000", "001", "002"]))
462 .row_header_column_index(-1)
463 .build()
464 .unwrap_err();
465 assert_eq!(err.object(), "DataTable");
466
467 let errors = err.field("row_header_column_index");
468 assert!(errors.includes(ValidationErrorKind::MinIntegerValue(0)));
469 }
470
471 fn row<T: Into<DataTableCell>>(cells: Vec<T>) -> DataTableRow {
472 DataTableRow::from_iter(cells)
473 }
474
475 fn rich_text_row(cells: Vec<&str>) -> DataTableRow {
476 let cells = cells
477 .into_iter()
478 .map(|text| {
479 RichText::builder()
480 .element(
481 RichTextSection::builder()
482 .element(
483 RichTextElementText::builder()
484 .text(text)
485 .build()
486 .unwrap(),
487 )
488 .build()
489 .unwrap(),
490 )
491 .build()
492 .unwrap()
493 })
494 .collect::<Vec<RichText>>();
495 row(cells)
496 }
497}