Skip to main content

slack_messaging/blocks/data_visualization/
mod.rs

1use crate::validators::*;
2
3use paste::paste;
4use serde::Serialize;
5use slack_messaging_derive::Builder;
6
7/// Charts and their related components.
8pub mod charts;
9
10/// Re-export of all chart types and related components.
11pub mod prelude {
12    pub use super::charts::*;
13    pub use super::{Chart, DataVisualization};
14}
15
16use charts::{AreaChart, BarChart, LineChart, PieChart};
17
18/// Each chart type supported by the chart field of the [DataVisualization]
19#[derive(Debug, Clone, Serialize, PartialEq)]
20#[serde(untagged)]
21pub enum Chart {
22    /// [Pie chart](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block#pie)
23    Pie(PieChart),
24
25    /// [Bar chart](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block#bar)
26    Bar(BarChart),
27
28    /// [Area chart](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block#area)
29    Area(AreaChart),
30
31    /// [Line chart](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block#line)
32    Line(LineChart),
33}
34
35macro_rules! impl_chart_from {
36    ($($var:tt,)*) => {
37        paste! {
38            $(
39                impl From<[<$var Chart>]> for Chart {
40                    fn from(value: [<$var Chart>]) -> Self {
41                        Chart::$var(value)
42                    }
43                }
44            )*
45        }
46    };
47}
48
49impl_chart_from!(Pie, Bar, Area, Line,);
50
51/// [Data visualization
52/// block](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block/)
53/// representation.
54///
55/// # Fields and Validations
56///
57/// For more details, see the [official
58/// documentation](https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block).
59///
60/// | Field | Type | Required | Validation |
61/// |-------|------|----------|------------|
62/// | title | String | Yes | Max length 50 characters |
63/// | chart | [Chart] | Yes | N/A |
64/// | block_id | String | No | Max length 255 characters |
65///
66/// # Example 1) Pie chart
67///
68/// ```
69/// use slack_messaging::blocks::data_visualization::prelude::*;
70/// # use std::error::Error;
71///
72/// # fn try_main() -> Result<(), Box<dyn Error>> {
73/// let data_viz = DataVisualization::builder()
74///     .title("My Favorite Candy Bars")
75///     .chart(
76///         PieChart::builder()
77///             .segments(segments(vec![
78///                 ("Kit Kat", 45),
79///                 ("Twix", 28),
80///                 ("Crunch", 18),
81///                 ("Milky Way", 9),
82///             ])?)
83///             .build()?
84///     )
85///     .build()?;
86///
87/// let expected = serde_json::json!({
88///     "type": "data_visualization",
89///     "title": "My Favorite Candy Bars",
90///     "chart": {
91///         "type": "pie",
92///         "segments": [
93///             { "label": "Kit Kat", "value": 45 },
94///             { "label": "Twix", "value": 28 },
95///             { "label": "Crunch", "value": 18 },
96///             { "label": "Milky Way", "value": 9 }
97///         ]
98///     }
99/// });
100///
101/// let json = serde_json::to_value(data_viz).unwrap();
102///
103/// assert_eq!(json, expected);
104/// #     Ok(())
105/// # }
106/// # fn main() {
107/// #     try_main().unwrap()
108/// # }
109/// ```
110///
111/// # Example 2) Bar chart
112///
113/// ```
114/// use slack_messaging::blocks::data_visualization::prelude::*;
115/// # use std::error::Error;
116///
117/// # fn try_main() -> Result<(), Box<dyn Error>> {
118/// let data_viz = DataVisualization::builder()
119///     .title("My Favorite Pies by Percentage of Tastiness")
120///     .chart(
121///         BarChart::builder()
122///             .series(vec![
123///                 DataSeries::builder()
124///                     .name("Pies")
125///                     .data(data_points(vec![
126///                         ("Strawberry Rhubarb", 85),
127///                         ("Pumpkin", 70),
128///                         ("Lemon Meringue", 72),
129///                         ("Blueberry", 90),
130///                         ("Key Lime", 56),
131///                     ])?)
132///                     .build()?
133///             ])
134///             .axis_config(
135///                 AxisConfig::builder()
136///                     .categories(vec![
137///                         "Strawberry Rhubarb",
138///                         "Pumpkin",
139///                         "Lemon Meringue",
140///                         "Blueberry",
141///                         "Key Lime",
142///                     ])
143///                     .x_label("Pies")
144///                     .y_label("Percentage of Tastiness")
145///                     .build()?
146///             )
147///             .build()?
148///     )
149///     .build()?;
150///
151/// let expected = serde_json::json!({
152///     "type": "data_visualization",
153///     "title": "My Favorite Pies by Percentage of Tastiness",
154///     "chart": {
155///         "type": "bar",
156///         "series": [
157///             {
158///                 "name": "Pies",
159///                 "data": [
160///                     { "label": "Strawberry Rhubarb", "value": 85 },
161///                     { "label": "Pumpkin", "value": 70 },
162///                     { "label": "Lemon Meringue", "value": 72 },
163///                     { "label": "Blueberry", "value": 90 },
164///                     { "label": "Key Lime", "value": 56 }
165///                 ]
166///             }
167///         ],
168///         "axis_config": {
169///             "categories": [
170///                 "Strawberry Rhubarb",
171///                 "Pumpkin",
172///                 "Lemon Meringue",
173///                 "Blueberry",
174///                 "Key Lime"
175///             ],
176///             "x_label": "Pies",
177///             "y_label": "Percentage of Tastiness"
178///         }
179///     }
180/// });
181///
182/// let json = serde_json::to_value(data_viz).unwrap();
183///
184/// assert_eq!(json, expected);
185/// #     Ok(())
186/// # }
187/// # fn main() {
188/// #     try_main().unwrap()
189/// # }
190/// ```
191///
192/// # Example 3) Area chart
193///
194/// ```
195/// use slack_messaging::blocks::data_visualization::prelude::*;
196/// # use std::error::Error;
197///
198/// # fn try_main() -> Result<(), Box<dyn Error>> {
199/// let data_viz = DataVisualization::builder()
200///     .title("Daily Active Users")
201///     .chart(
202///         AreaChart::builder()
203///             .series(vec![
204///                 DataSeries::builder()
205///                     .name("Pied Piper Free Tier")
206///                     .data(data_points(vec![
207///                         ("Mon", 12000),
208///                         ("Tue", 13500),
209///                         ("Wed", 15200),
210///                         ("Thu", 14800),
211///                         ("Fri", 16400),
212///                     ])?)
213///                     .build()?,
214///                 DataSeries::builder()
215///                     .name("Pied Piper Paid Tier")
216///                     .data(data_points(vec![
217///                         ("Mon", 4500),
218///                         ("Tue", 4800),
219///                         ("Wed", 5100),
220///                         ("Thu", 5600),
221///                         ("Fri", 6200),
222///                     ])?)
223///                     .build()?,
224///             ])
225///             .axis_config(
226///                 AxisConfig::builder()
227///                     .categories(vec!["Mon", "Tue", "Wed", "Thu", "Fri"])
228///                     .x_label("Day")
229///                     .y_label("Users")
230///                     .build()?
231///             )
232///             .build()?
233///     )
234///     .build()?;
235///
236/// let expected = serde_json::json!({
237///     "type": "data_visualization",
238///     "title": "Daily Active Users",
239///     "chart": {
240///         "type": "area",
241///         "series": [
242///             {
243///                 "name": "Pied Piper Free Tier",
244///                 "data": [
245///                     { "label": "Mon", "value": 12000 },
246///                     { "label": "Tue", "value": 13500 },
247///                     { "label": "Wed", "value": 15200 },
248///                     { "label": "Thu", "value": 14800 },
249///                     { "label": "Fri", "value": 16400 }
250///                 ]
251///             },
252///             {
253///                 "name": "Pied Piper Paid Tier",
254///                 "data": [
255///                     { "label": "Mon", "value": 4500 },
256///                     { "label": "Tue", "value": 4800 },
257///                     { "label": "Wed", "value": 5100 },
258///                     { "label": "Thu", "value": 5600 },
259///                     { "label": "Fri", "value": 6200 }
260///                 ]
261///             }
262///         ],
263///         "axis_config": {
264///             "categories": ["Mon", "Tue", "Wed", "Thu", "Fri"],
265///             "x_label": "Day",
266///             "y_label": "Users"
267///         }
268///     }
269/// });
270///
271/// let json = serde_json::to_value(data_viz).unwrap();
272///
273/// assert_eq!(json, expected);
274/// #     Ok(())
275/// # }
276/// # fn main() {
277/// #     try_main().unwrap()
278/// # }
279/// ```
280///
281/// # Example 4) Line chart
282///
283/// ```
284/// use slack_messaging::blocks::data_visualization::prelude::*;
285/// # use std::error::Error;
286///
287/// # fn try_main() -> Result<(), Box<dyn Error>> {
288/// let data_viz = DataVisualization::builder()
289///     .title("Weekly Paper Sales")
290///     .chart(
291///         LineChart::builder()
292///             .series(vec![
293///                 DataSeries::builder()
294///                     .name("Website")
295///                     .data(data_points(vec![
296///                         ("Week 1", 32000),
297///                         ("Week 2", 35000),
298///                         ("Week 3", 29000),
299///                         ("Week 4", 41000),
300///                         ("Week 5", 45000),
301///                     ])?)
302///                     .build()?,
303///                 DataSeries::builder()
304///                     .name("In-store")
305///                     .data(data_points(vec![
306///                         ("Week 1", 17000),
307///                         ("Week 2", 20000),
308///                         ("Week 3", 15000),
309///                         ("Week 4", 22000),
310///                         ("Week 5", 30000),
311///                     ])?)
312///                     .build()?,
313///             ])
314///             .axis_config(
315///                 AxisConfig::builder()
316///                     .categories(vec![
317///                         "Week 1",
318///                         "Week 2",
319///                         "Week 3",
320///                         "Week 4",
321///                         "Week 5",
322///                     ])
323///                     .x_label("Week")
324///                     .y_label("Paper Sales (USD)")
325///                     .build()?
326///             )
327///             .build()?
328///     )
329///     .build()?;
330///
331/// let expected = serde_json::json!({
332///     "type": "data_visualization",
333///     "title": "Weekly Paper Sales",
334///     "chart": {
335///         "type": "line",
336///         "series": [
337///             {
338///                 "name": "Website",
339///                 "data": [
340///                     { "label": "Week 1", "value": 32000 },
341///                     { "label": "Week 2", "value": 35000 },
342///                     { "label": "Week 3", "value": 29000 },
343///                     { "label": "Week 4", "value": 41000 },
344///                     { "label": "Week 5", "value": 45000 }
345///                 ]
346///             },
347///             {
348///                 "name": "In-store",
349///                 "data": [
350///                     { "label": "Week 1", "value": 17000 },
351///                     { "label": "Week 2", "value": 20000 },
352///                     { "label": "Week 3", "value": 15000 },
353///                     { "label": "Week 4", "value": 22000 },
354///                     { "label": "Week 5", "value": 30000 }
355///                 ]
356///             }
357///         ],
358///         "axis_config": {
359///             "categories": ["Week 1", "Week 2", "Week 3", "Week 4", "Week 5"],
360///             "x_label": "Week",
361///             "y_label": "Paper Sales (USD)"
362///         }
363///     }
364/// });
365///
366/// let json = serde_json::to_value(data_viz).unwrap();
367///
368/// assert_eq!(json, expected);
369/// #     Ok(())
370/// # }
371/// # fn main() {
372/// #     try_main().unwrap()
373/// # }
374/// ```
375#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
376#[serde(tag = "type", rename = "data_visualization")]
377pub struct DataVisualization {
378    #[builder(validate("required", "text::max_50"))]
379    pub(crate) title: Option<String>,
380
381    #[builder(validate("required"))]
382    pub(crate) chart: Option<Chart>,
383
384    #[serde(skip_serializing_if = "Option::is_none")]
385    #[builder(validate("text::max_255"))]
386    pub(crate) block_id: Option<String>,
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use super::prelude::*;
393    use crate::errors::*;
394
395    #[test]
396    fn it_implements_builder() {
397        let expected = DataVisualization {
398            title: Some("My Favorite Candy Bars".into()),
399            chart: Some(chart()),
400            block_id: Some("data_viz_0".into()),
401        };
402
403        let val = DataVisualization::builder()
404            .set_title(Some("My Favorite Candy Bars"))
405            .set_chart(Some(chart()))
406            .set_block_id(Some("data_viz_0"))
407            .build()
408            .unwrap();
409
410        assert_eq!(val, expected);
411
412        let val = DataVisualization::builder()
413            .title("My Favorite Candy Bars")
414            .chart(chart())
415            .block_id("data_viz_0")
416            .build()
417            .unwrap();
418
419        assert_eq!(val, expected);
420    }
421
422    #[test]
423    fn it_requires_title() {
424        let err = DataVisualization::builder()
425            .chart(chart())
426            .build()
427            .unwrap_err();
428        assert_eq!(err.object(), "DataVisualization");
429
430        let errors = err.field("title");
431        assert!(errors.includes(ValidationErrorKind::Required));
432    }
433
434    #[test]
435    fn it_requires_title_field_to_be_50_characters_or_fewer() {
436        let err = DataVisualization::builder()
437            .title("a".repeat(51))
438            .chart(chart())
439            .build()
440            .unwrap_err();
441        assert_eq!(err.object(), "DataVisualization");
442
443        let errors = err.field("title");
444        assert!(errors.includes(ValidationErrorKind::MaxTextLength(50)));
445    }
446
447    #[test]
448    fn it_requires_chart() {
449        let err = DataVisualization::builder()
450            .title("My Favorite Candy Bars")
451            .build()
452            .unwrap_err();
453        assert_eq!(err.object(), "DataVisualization");
454
455        let errors = err.field("chart");
456        assert!(errors.includes(ValidationErrorKind::Required));
457    }
458
459    #[test]
460    fn it_requires_block_id_field_to_be_255_characters_or_fewer() {
461        let err = DataVisualization::builder()
462            .title("My Favorite Candy Bars")
463            .chart(chart())
464            .block_id("a".repeat(256))
465            .build()
466            .unwrap_err();
467        assert_eq!(err.object(), "DataVisualization");
468
469        let errors = err.field("block_id");
470        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
471    }
472
473    fn chart() -> Chart {
474        Chart::Pie(PieChart::builder()
475            .segments(segments(vec![
476                ("Kit Kat", 45),
477                ("Twix", 28),
478                ("Crunch", 18),
479                ("Milky Way", 9),
480            ]).unwrap())
481            .build()
482            .unwrap())
483    }
484}