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}