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}