slack_messaging/blocks/
actions.rs

1use crate::blocks::elements::{
2    Button, Checkboxes, DatePicker, DatetimePicker, MultiSelectMenuConversations,
3    MultiSelectMenuExternalDataSource, MultiSelectMenuPublicChannels, MultiSelectMenuStaticOptions,
4    MultiSelectMenuUsers, OverflowMenu, RadioButtonGroup, SelectMenuConversations,
5    SelectMenuExternalDataSource, SelectMenuPublicChannels, SelectMenuStaticOptions,
6    SelectMenuUsers, TimePicker, WorkflowButton,
7};
8use crate::validators::*;
9
10use serde::Serialize;
11use slack_messaging_derive::Builder;
12
13/// [Actions block](https://docs.slack.dev/reference/block-kit/blocks/actions-block)
14/// representation.
15///
16/// # Fields and Validations
17///
18/// For more details, see the [official
19/// documentation](https://docs.slack.dev/reference/block-kit/blocks/actions-block).
20///
21/// | Field | Type | Required | Validation |
22/// |-------|------|----------|------------|
23/// | elements | Vec<[ActionsElement]> | Yes | Maximum of 25 items |
24/// | block_id | String | No | Maximum 255 characters |
25///
26/// # Example
27///
28/// The following is reproduction of [the 1st sample actions](https://docs.slack.dev/reference/block-kit/blocks/actions-block#examples).
29///
30/// ```
31/// use slack_messaging::plain_text;
32/// use slack_messaging::blocks::Actions;
33/// use slack_messaging::blocks::elements::{Button, SelectMenuStaticOptions};
34/// use slack_messaging::composition_objects::Opt;
35/// # use std::error::Error;
36///
37/// # fn try_main() -> Result<(), Box<dyn Error>> {
38/// let actions = Actions::builder()
39///     .block_id("actions1")
40///     .element(
41///         SelectMenuStaticOptions::builder()
42///             .action_id("select_2")
43///             .placeholder(plain_text!("Which witch is the witchiest witch?")?)
44///             .options(
45///                 vec![
46///                     Opt::builder()
47///                         .text(plain_text!("Matilda")?)
48///                         .value("matilda")
49///                         .build()?,
50///                     Opt::builder()
51///                         .text(plain_text!("Glinda")?)
52///                         .value("glinda")
53///                         .build()?,
54///                     Opt::builder()
55///                         .text(plain_text!("Granny Weatherwax")?)
56///                         .value("grannyWeatherwax")
57///                         .build()?,
58///                     Opt::builder()
59///                         .text(plain_text!("Hermione")?)
60///                         .value("hermione")
61///                         .build()?,
62///                 ]
63///             )
64///             .build()?
65///     )
66///     .element(
67///         Button::builder()
68///             .action_id("button_1")
69///             .value("cancel")
70///             .text(plain_text!("Cancel")?)
71///             .build()?
72///     )
73///     .build()?;
74///
75/// let expected = serde_json::json!({
76///     "type": "actions",
77///     "block_id": "actions1",
78///     "elements": [
79///         {
80///             "type": "static_select",
81///             "action_id": "select_2",
82///             "placeholder": {
83///                 "type": "plain_text",
84///                 "text": "Which witch is the witchiest witch?"
85///             },
86///             "options": [
87///                 {
88///                     "text": {
89///                         "type": "plain_text",
90///                         "text": "Matilda"
91///                     },
92///                     "value": "matilda"
93///                 },
94///                 {
95///                     "text": {
96///                         "type": "plain_text",
97///                         "text": "Glinda"
98///                     },
99///                     "value": "glinda"
100///                 },
101///                 {
102///                     "text": {
103///                         "type": "plain_text",
104///                         "text": "Granny Weatherwax"
105///                     },
106///                     "value": "grannyWeatherwax"
107///                 },
108///                 {
109///                     "text": {
110///                         "type": "plain_text",
111///                         "text": "Hermione"
112///                     },
113///                     "value": "hermione"
114///                 }
115///             ]
116///         },
117///         {
118///             "type": "button",
119///             "text": {
120///                 "type": "plain_text",
121///                 "text": "Cancel"
122///             },
123///             "value": "cancel",
124///             "action_id": "button_1"
125///         }
126///     ]
127/// });
128///
129/// let json = serde_json::to_value(actions).unwrap();
130///
131/// assert_eq!(json, expected);
132/// #     Ok(())
133/// # }
134/// # fn main() {
135/// #     try_main().unwrap()
136/// # }
137/// ```
138///
139/// And the following is the [2nd sample actions](https://docs.slack.dev/reference/block-kit/blocks/actions-block#examples).
140///
141/// ```
142/// use slack_messaging::plain_text;
143/// use slack_messaging::blocks::Actions;
144/// use slack_messaging::blocks::elements::{Button, DatePicker, OverflowMenu};
145/// use slack_messaging::composition_objects::Opt;
146/// # use std::error::Error;
147///
148/// # fn try_main() -> Result<(), Box<dyn Error>> {
149/// let actions = Actions::builder()
150///     .block_id("actionblock789")
151///     .element(
152///         DatePicker::builder()
153///             .action_id("datepicker123")
154///             .initial_date("1990-04-28")
155///             .placeholder(plain_text!("Select a date")?)
156///             .build()?
157///     )
158///     .element(
159///         OverflowMenu::builder()
160///             .action_id("overflow")
161///             .option(
162///                 Opt::builder()
163///                     .text(plain_text!("*this is plain_text text*")?)
164///                     .value("value-0")
165///                     .build()?
166///             )
167///             .option(
168///                 Opt::builder()
169///                     .text(plain_text!("*this is plain_text text*")?)
170///                     .value("value-1")
171///                     .build()?
172///             )
173///             .option(
174///                 Opt::builder()
175///                     .text(plain_text!("*this is plain_text text*")?)
176///                     .value("value-2")
177///                     .build()?
178///             )
179///             .option(
180///                 Opt::builder()
181///                     .text(plain_text!("*this is plain_text text*")?)
182///                     .value("value-3")
183///                     .build()?
184///             )
185///             .option(
186///                 Opt::builder()
187///                     .text(plain_text!("*this is plain_text text*")?)
188///                     .value("value-4")
189///                     .build()?
190///             )
191///             .build()?
192///     )
193///     .element(
194///         Button::builder()
195///             .action_id("button")
196///             .value("click_me_123")
197///             .text(plain_text!("Click Me")?)
198///             .build()?
199///     )
200///     .build()?;
201///
202/// let expected = serde_json::json!({
203///     "type": "actions",
204///     "block_id": "actionblock789",
205///     "elements": [
206///         {
207///             "type": "datepicker",
208///             "action_id": "datepicker123",
209///             "initial_date": "1990-04-28",
210///             "placeholder": {
211///                 "type": "plain_text",
212///                 "text": "Select a date"
213///             }
214///         },
215///         {
216///             "type": "overflow",
217///             "action_id": "overflow",
218///             "options": [
219///                 {
220///                     "text": {
221///                         "type": "plain_text",
222///                         "text": "*this is plain_text text*"
223///                     },
224///                     "value": "value-0"
225///                 },
226///                 {
227///                     "text": {
228///                         "type": "plain_text",
229///                         "text": "*this is plain_text text*"
230///                     },
231///                     "value": "value-1"
232///                 },
233///                 {
234///                     "text": {
235///                         "type": "plain_text",
236///                         "text": "*this is plain_text text*"
237///                     },
238///                     "value": "value-2"
239///                 },
240///                 {
241///                     "text": {
242///                         "type": "plain_text",
243///                         "text": "*this is plain_text text*"
244///                     },
245///                     "value": "value-3"
246///                 },
247///                 {
248///                     "text": {
249///                         "type": "plain_text",
250///                         "text": "*this is plain_text text*"
251///                     },
252///                     "value": "value-4"
253///                 }
254///             ]
255///         },
256///         {
257///             "type": "button",
258///             "text": {
259///                 "type": "plain_text",
260///                 "text": "Click Me"
261///             },
262///             "value": "click_me_123",
263///             "action_id": "button"
264///         }
265///     ]
266/// });
267///
268/// let json = serde_json::to_value(actions).unwrap();
269///
270/// assert_eq!(json, expected);
271/// #     Ok(())
272/// # }
273/// # fn main() {
274/// #     try_main().unwrap()
275/// # }
276/// ```
277///
278#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
279#[serde(tag = "type", rename = "actions")]
280pub struct Actions {
281    #[builder(push_item = "element", validate("required", "list::max_item_25"))]
282    pub(crate) elements: Option<Vec<ActionsElement>>,
283
284    #[serde(skip_serializing_if = "Option::is_none")]
285    #[builder(validate("text::max_255"))]
286    pub(crate) block_id: Option<String>,
287}
288
289/// Objects that can be an element of the [Actions]'s elements field.
290#[derive(Debug, Clone, Serialize, PartialEq)]
291#[serde(untagged)]
292pub enum ActionsElement {
293    /// [Button element](https://docs.slack.dev/reference/block-kit/block-elements/button-element)
294    /// representation
295    Button(Box<Button>),
296
297    /// [Checkbox group](https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element)
298    /// representation
299    Checkboxes(Box<Checkboxes>),
300
301    /// [Date picker element](https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element)
302    /// representation
303    DatePicker(Box<DatePicker>),
304
305    /// [Datetime picker element](https://docs.slack.dev/reference/block-kit/block-elements/datetime-picker-element)
306    /// representation
307    DatetimePicker(Box<DatetimePicker>),
308
309    /// [Multi select menu of static options](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select)
310    /// representation
311    MultiSelectMenuStaticOptions(Box<MultiSelectMenuStaticOptions>),
312
313    /// [Multi select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select)
314    /// representation
315    MultiSelectMenuExternalDataSource(Box<MultiSelectMenuExternalDataSource>),
316
317    /// [Multi select menu of users](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select)
318    /// representation
319    MultiSelectMenuUsers(Box<MultiSelectMenuUsers>),
320
321    /// [Multi select menu of conversations](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#conversation_multi_select)
322    /// representation
323    MultiSelectMenuConversations(Box<MultiSelectMenuConversations>),
324
325    /// [Multi select menu of public channels](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select)
326    /// representation
327    MultiSelectMenuPublicChannels(Box<MultiSelectMenuPublicChannels>),
328
329    /// [Overflow menu element](https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element)
330    /// representation
331    OverflowMenu(Box<OverflowMenu>),
332
333    /// [Radio buton group element](https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element)
334    /// representation
335    RadioButtonGroup(Box<RadioButtonGroup>),
336
337    /// [Select menu of static options](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select)
338    /// representation
339    SelectMenuStaticOptions(Box<SelectMenuStaticOptions>),
340
341    /// [Select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select)
342    /// representation
343    SelectMenuExternalDataSource(Box<SelectMenuExternalDataSource>),
344
345    /// [Select menu of users](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select)
346    /// representation
347    SelectMenuUsers(Box<SelectMenuUsers>),
348
349    /// [Select menu of conversations](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#conversations_select)
350    /// representation
351    SelectMenuConversations(Box<SelectMenuConversations>),
352
353    /// [Select menu of public channels](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#channels_select)
354    /// representation
355    SelectMenuPublicChannels(Box<SelectMenuPublicChannels>),
356
357    /// [Time picker element](https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element)
358    /// representation
359    TimePicker(Box<TimePicker>),
360
361    /// [Workflow button element](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element)
362    /// representation
363    WorkflowButton(Box<WorkflowButton>),
364}
365
366macro_rules! actions_from {
367    ($($ty:ident,)*) => {
368        $(
369            impl From<$ty> for ActionsElement {
370                fn from(value: $ty) -> Self {
371                    Self::$ty(Box::new(value))
372                }
373            }
374         )*
375    }
376}
377
378actions_from! {
379    Button,
380    Checkboxes,
381    DatePicker,
382    DatetimePicker,
383    MultiSelectMenuStaticOptions,
384    MultiSelectMenuExternalDataSource,
385    MultiSelectMenuUsers,
386    MultiSelectMenuConversations,
387    MultiSelectMenuPublicChannels,
388    OverflowMenu,
389    RadioButtonGroup,
390    SelectMenuStaticOptions,
391    SelectMenuExternalDataSource,
392    SelectMenuUsers,
393    SelectMenuConversations,
394    SelectMenuPublicChannels,
395    TimePicker,
396    WorkflowButton,
397}
398
399#[cfg(test)]
400mod tests {
401    use super::*;
402    use crate::blocks::elements::test_helpers::*;
403    use crate::errors::*;
404
405    #[test]
406    fn it_implements_builder() {
407        let expected = Actions {
408            block_id: Some("actions_0".into()),
409            elements: Some(vec![datepicker().into(), btn("button_0", "value_0").into()]),
410        };
411
412        let val = Actions::builder()
413            .set_block_id(Some("actions_0"))
414            .set_elements(Some(vec![
415                datepicker().into(),
416                btn("button_0", "value_0").into(),
417            ]))
418            .build()
419            .unwrap();
420
421        assert_eq!(val, expected);
422
423        let val = Actions::builder()
424            .block_id("actions_0")
425            .elements(vec![datepicker().into(), btn("button_0", "value_0").into()])
426            .build()
427            .unwrap();
428
429        assert_eq!(val, expected);
430    }
431
432    #[test]
433    fn it_implements_push_item_method() {
434        let expected = Actions {
435            block_id: None,
436            elements: Some(vec![datepicker().into(), btn("button_0", "value_0").into()]),
437        };
438
439        let val = Actions::builder()
440            .element(datepicker())
441            .element(btn("button_0", "value_0"))
442            .build()
443            .unwrap();
444
445        assert_eq!(val, expected);
446    }
447
448    #[test]
449    fn it_requries_elements_field() {
450        let err = Actions::builder().build().unwrap_err();
451        assert_eq!(err.object(), "Actions");
452
453        let errors = err.field("elements");
454        assert!(errors.includes(ValidationErrorKind::Required));
455    }
456
457    #[test]
458    fn it_requires_elements_list_size_less_than_25() {
459        let elements: Vec<ActionsElement> = (0..26).map(|_| btn("name", "value").into()).collect();
460        let err = Actions::builder().elements(elements).build().unwrap_err();
461        assert_eq!(err.object(), "Actions");
462
463        let errors = err.field("elements");
464        assert!(errors.includes(ValidationErrorKind::MaxArraySize(25)));
465    }
466
467    #[test]
468    fn it_requires_block_id_less_than_255_characters_long() {
469        let err = Actions::builder()
470            .block_id("a".repeat(256))
471            .element(datepicker())
472            .build()
473            .unwrap_err();
474        assert_eq!(err.object(), "Actions");
475
476        let errors = err.field("block_id");
477        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
478    }
479}