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::{RichText, DataTable};
36/// use slack_messaging::blocks::rich_text::RichTextSection;
37/// use slack_messaging::blocks::rich_text::types::{RichTextElementText, RichTextStyle};
38/// use slack_messaging::blocks::data_table::DataTableRow;
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(push_item = "row", validate("required", "list::min_item_2", "list::max_item_101"))]
253    pub(crate) rows: Option<Vec<DataTableRow>>,
254
255    #[builder(validate("required"))]
256    pub(crate) caption: Option<String>,
257
258    #[serde(skip_serializing_if = "Option::is_none")]
259    #[builder(validate("integer::min_1", "integer::max_100"))]
260    pub(crate) page_size: Option<i64>,
261
262    #[serde(skip_serializing_if = "Option::is_none")]
263    #[builder(validate("integer::min_0"))]
264    pub(crate) row_header_column_index: Option<i64>,
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::errors::*;
271
272    #[test]
273    fn it_implements_builder() {
274        let expected = DataTable {
275            block_id: Some("table_0".into()),
276            caption: Some("A Fabulous Table".into()),
277            rows: Some(vec![
278                row(vec!["foo", "bar", "baz"]),
279                row(vec!["000", "001", "002"]),
280            ]),
281            page_size: Some(10),
282            row_header_column_index: Some(0),
283        };
284
285        let val = DataTable::builder()
286            .set_block_id(Some("table_0"))
287            .set_caption(Some("A Fabulous Table"))
288            .set_rows(Some(vec![
289                row(vec!["foo", "bar", "baz"]),
290                row(vec!["000", "001", "002"]),
291            ]))
292            .set_page_size(Some(10))
293            .set_row_header_column_index(Some(0))
294            .build()
295            .unwrap();
296
297        assert_eq!(val, expected);
298
299        let val = DataTable::builder()
300            .block_id("table_0")
301            .caption("A Fabulous Table")
302            .rows(vec![
303                row(vec!["foo", "bar", "baz"]),
304                row(vec!["000", "001", "002"]),
305            ])
306            .page_size(10)
307            .row_header_column_index(0)
308            .build()
309            .unwrap();
310
311        assert_eq!(val, expected);
312    }
313
314    #[test]
315    fn it_implements_push_item_method_for_rows_field() {
316        let expected = DataTable {
317            block_id: None,
318            caption: Some("A Fabulous Table".into()),
319            rows: Some(vec![
320                row(vec!["foo", "bar", "baz"]),
321                row(vec!["000", "001", "002"]),
322            ]),
323            page_size: None,
324            row_header_column_index: None,
325        };
326
327        let val = DataTable::builder()
328            .caption("A Fabulous Table")
329            .row(row(vec!["foo", "bar", "baz"]))
330            .row(row(vec!["000", "001", "002"]))
331            .build()
332            .unwrap();
333
334        assert_eq!(val, expected);
335    }
336
337    #[test]
338    fn it_requires_block_id_to_be_max_255_characters() {
339        let err = DataTable::builder()
340            .block_id("a".repeat(256))
341            .caption("A Fabulous Table")
342            .row(row(vec!["foo", "bar", "baz"]))
343            .row(row(vec!["000", "001", "002"]))
344            .build()
345            .unwrap_err();
346        assert_eq!(err.object(), "DataTable");
347
348        let errors = err.field("block_id");
349        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
350    }
351
352    #[test]
353    fn it_requires_rows_field() {
354        let err = DataTable::builder()
355            .caption("A Fabulous Table")
356            .build()
357            .unwrap_err();
358        assert_eq!(err.object(), "DataTable");
359
360        let errors = err.field("rows");
361        assert!(errors.includes(ValidationErrorKind::Required));
362    }
363
364    #[test]
365    fn it_requires_rows_field_to_have_at_least_2_items() {
366        let err = DataTable::builder()
367            .caption("A Fabulous Table")
368            .row(row(vec!["foo", "bar", "baz"]))
369            .build()
370            .unwrap_err();
371        assert_eq!(err.object(), "DataTable");
372
373        let errors = err.field("rows");
374        assert!(errors.includes(ValidationErrorKind::MinArraySize(2)));
375    }
376
377    #[test]
378    fn it_requires_rows_field_to_have_at_most_101_items() {
379        let rows: Vec<DataTableRow> = (0..102).map(|_| row(vec!["foo", "bar", "baz"])).collect();
380        let err = DataTable::builder().rows(rows).build().unwrap_err();
381        assert_eq!(err.object(), "DataTable");
382
383        let errors = err.field("rows");
384        assert!(errors.includes(ValidationErrorKind::MaxArraySize(101)));
385    }
386
387    #[test]
388    fn it_requires_caption_field() {
389        let err = DataTable::builder()
390            .row(row(vec!["foo", "bar", "baz"]))
391            .row(row(vec!["000", "001", "002"]))
392            .build()
393            .unwrap_err();
394        assert_eq!(err.object(), "DataTable");
395
396        let errors = err.field("caption");
397        assert!(errors.includes(ValidationErrorKind::Required));
398    }
399
400    #[test]
401    fn it_requires_page_size_to_be_at_least_1() {
402        let err = DataTable::builder()
403            .caption("A Fabulous Table")
404            .row(row(vec!["foo", "bar", "baz"]))
405            .row(row(vec!["000", "001", "002"]))
406            .page_size(0)
407            .build()
408            .unwrap_err();
409        assert_eq!(err.object(), "DataTable");
410
411        let errors = err.field("page_size");
412        assert!(errors.includes(ValidationErrorKind::MinIntegerValue(1)));
413    }
414
415    #[test]
416    fn it_requires_page_size_to_be_at_most_100() {
417        let err = DataTable::builder()
418            .caption("A Fabulous Table")
419            .row(row(vec!["foo", "bar", "baz"]))
420            .row(row(vec!["000", "001", "002"]))
421            .page_size(101)
422            .build()
423            .unwrap_err();
424        assert_eq!(err.object(), "DataTable");
425
426        let errors = err.field("page_size");
427        assert!(errors.includes(ValidationErrorKind::MaxIntegerValue(100)));
428    }
429
430    #[test]
431    fn it_requires_row_header_column_index_to_be_at_least_0() {
432        let err = DataTable::builder()
433            .caption("A Fabulous Table")
434            .row(row(vec!["foo", "bar", "baz"]))
435            .row(row(vec!["000", "001", "002"]))
436            .row_header_column_index(-1)
437            .build()
438            .unwrap_err();
439        assert_eq!(err.object(), "DataTable");
440
441        let errors = err.field("row_header_column_index");
442        assert!(errors.includes(ValidationErrorKind::MinIntegerValue(0)));
443    }
444
445    fn row<T: Into<DataTableCell>>(cells: Vec<T>) -> DataTableRow {
446        DataTableRow::from_iter(cells)
447    }
448}