Skip to main content

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}