Skip to main content

slack_messaging/blocks/data_table/
mod.rs

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