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}