slack_blocks/blocks/
actions.rs

1//! # Actions Block
2//!
3//! [slack api docs 🔗]
4//!
5//! A block that is used to hold interactive [elements 🔗]
6//!
7//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#actions
8//! [elements 🔗]: https://api.slack.com/reference/messaging/block-elements
9
10use std::{borrow::Cow, convert::TryFrom};
11
12use serde::{Deserialize, Serialize};
13#[cfg(feature = "validation")]
14use validator::Validate;
15
16#[cfg(feature = "validation")]
17use crate::val_helpr::*;
18use crate::{convert,
19            elems::{select,
20                    BlockElement,
21                    Button,
22                    Checkboxes,
23                    DatePicker,
24                    Overflow,
25                    Radio,
26                    TextInput}};
27
28/// # Actions Block
29///
30/// [slack api docs 🔗]
31///
32/// A block that is used to hold interactive [elements 🔗]
33///
34/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#actions
35/// [elements 🔗]: https://api.slack.com/reference/messaging/block-elements
36#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize)]
37#[cfg_attr(feature = "validation", derive(Validate))]
38pub struct Actions<'a> {
39  #[cfg_attr(feature = "validation", validate(length(max = 5)))]
40  elements: Vec<SupportedElement<'a>>,
41
42  #[serde(skip_serializing_if = "Option::is_none")]
43  #[cfg_attr(feature = "validation",
44             validate(custom = "super::validate_block_id"))]
45  block_id: Option<Cow<'a, str>>,
46}
47
48impl<'a> Actions<'a> {
49  /// Build a new Actions block.
50  ///
51  /// For example, see docs for ActionsBuilder.
52  pub fn builder() -> build::ActionsBuilderInit<'a> {
53    build::ActionsBuilderInit::new()
54  }
55
56  /// Validate that this Section block agrees with Slack's model requirements
57  ///
58  /// # Errors
59  /// - If `block_id` longer than 255 chars
60  /// - If `elements` contains more than 5 elements
61  ///
62  /// # Example
63  /// ```
64  /// use slack_blocks::{blocks, compose, elems::Button};
65  ///
66  /// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
67  ///
68  /// let block =
69  ///   blocks::Actions::builder().element(Button::builder().text("Click me")
70  ///                                                       .action_id("btn")
71  ///                                                       .build())
72  ///                             .block_id(long_string)
73  ///                             .build();
74  ///
75  /// assert!(matches!(block.validate(), Err(_)));
76  /// ```
77  #[cfg(feature = "validation")]
78  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
79  pub fn validate(&self) -> ValidationResult {
80    Validate::validate(self)
81  }
82}
83
84/// Actions block builder
85pub mod build {
86  use std::marker::PhantomData;
87
88  use super::*;
89  use crate::build::*;
90
91  /// Compile-time markers for builder methods
92  #[allow(non_camel_case_types)]
93  pub mod method {
94    /// ActionsBuilder.elements
95    #[derive(Clone, Copy, Debug)]
96    pub struct elements;
97  }
98
99  /// Initial state for `ActionsBuilder`
100  pub type ActionsBuilderInit<'a> =
101    ActionsBuilder<'a, RequiredMethodNotCalled<method::elements>>;
102
103  /// Build an Actions block
104  ///
105  /// Allows you to construct safely, with compile-time checks
106  /// on required setter methods.
107  ///
108  /// # Required Methods
109  /// `ActionsBuilder::build()` is only available if these methods have been called:
110  ///  - `element`
111  ///
112  /// # Example
113  /// ```
114  /// use slack_blocks::{blocks::Actions, elems::Button};
115  ///
116  /// let block = Actions::builder().element(Button::builder().text("Click me!")
117  ///                                                         .action_id("clicked")
118  ///                                                         .build())
119  ///                               .build();
120  /// ```
121  #[derive(Debug)]
122  pub struct ActionsBuilder<'a, Elements> {
123    elements: Option<Vec<SupportedElement<'a>>>,
124    block_id: Option<Cow<'a, str>>,
125    state: PhantomData<Elements>,
126  }
127
128  impl<'a, E> ActionsBuilder<'a, E> {
129    /// Create a new ActionsBuilder
130    pub fn new() -> Self {
131      Self { elements: None,
132             block_id: None,
133             state: PhantomData::<_> }
134    }
135
136    /// Add an `element` (**Required**, can be called many times)
137    ///
138    /// Add an interactive [element object 🔗]
139    ///
140    /// For a list of `BlockElement` types that are supported, see `slack_blocks::blocks::actions::SupportedElement`.
141    ///
142    /// There is a maximum of 5 elements in each action block.
143    ///
144    /// [element object 🔗]: https://api.slack.com/reference/messaging/block-elements
145    pub fn element<El>(self,
146                       element: El)
147                       -> ActionsBuilder<'a, Set<method::elements>>
148      where El: Into<SupportedElement<'a>>
149    {
150      let mut elements = self.elements.unwrap_or_default();
151      elements.push(element.into());
152
153      ActionsBuilder { block_id: self.block_id,
154                       elements: Some(elements),
155                       state: PhantomData::<_> }
156    }
157
158    /// Invoked by `blox!` when a child element is passed to `<actions_block>`.
159    ///
160    /// Alias of `ActionsBuilder.element`.
161    #[cfg(feature = "blox")]
162    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
163    pub fn child<El>(self,
164                     element: El)
165                     -> ActionsBuilder<'a, Set<method::elements>>
166      where El: Into<SupportedElement<'a>>
167    {
168      self.element(element)
169    }
170
171    /// Set `block_id` (Optional)
172    ///
173    /// A string acting as a unique identifier for a block.
174    ///
175    /// You can use this `block_id` when you receive an interaction payload
176    /// to [identify the source of the action 🔗].
177    ///
178    /// If not specified, a `block_id` will be generated.
179    ///
180    /// Maximum length for this field is 255 characters.
181    ///
182    /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
183    pub fn block_id<S>(mut self, block_id: S) -> Self
184      where S: Into<Cow<'a, str>>
185    {
186      self.block_id = Some(block_id.into());
187      self
188    }
189  }
190
191  impl<'a> ActionsBuilder<'a, Set<method::elements>> {
192    /// All done building, now give me a darn actions block!
193    ///
194    /// > `no method name 'build' found for struct 'ActionsBuilder<...>'`?
195    /// Make sure all required setter methods have been called. See docs for `ActionsBuilder`.
196    ///
197    /// ```compile_fail
198    /// use slack_blocks::blocks::Actions;
199    ///
200    /// let foo = Actions::builder().build(); // Won't compile!
201    /// ```
202    ///
203    /// ```
204    /// use slack_blocks::{blocks::Actions, elems::Button};
205    ///
206    /// let block = Actions::builder().element(Button::builder().text("Click me!")
207    ///                                                         .action_id("clicked")
208    ///                                                         .build())
209    ///                               .build();
210    /// ```
211    pub fn build(self) -> Actions<'a> {
212      Actions { elements: self.elements.unwrap(),
213                block_id: self.block_id }
214    }
215  }
216}
217
218/// The Block Elements supported in an Action Block.
219///
220/// Supports:
221/// - Overflow
222/// - RadioButtons
223/// - Button
224/// - TextInput
225/// - Checkboxes
226/// - DatePicker
227/// - Select Menus:
228///   - PublicChannel
229///   - Conversation
230///   - External
231///   - Static
232///   - User
233#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
234pub struct SupportedElement<'a>(BlockElement<'a>);
235
236impl<'a> TryFrom<BlockElement<'a>> for self::SupportedElement<'a> {
237  type Error = super::UnsupportedElement<'a>;
238
239  fn try_from(el: BlockElement<'a>) -> Result<Self, Self::Error> {
240    use BlockElement as El;
241
242    let unsupported = |el| super::UnsupportedElement { context:
243                                                         format!("{}::Actions",
244                                                                 module_path!()),
245                                                       element: el };
246
247    match el {
248      | El::SelectPublicChannel(_)
249      | El::SelectConversation(_)
250      | El::SelectExternal(_)
251      | El::SelectStatic(_)
252      | El::SelectUser(_)
253      | El::Overflow(_)
254      | El::RadioButtons(_)
255      | El::Button(_)
256      | El::TextInput(_)
257      | El::Checkboxes(_)
258      | El::DatePicker(_) => Ok(SupportedElement(el)),
259      | _ => Err(unsupported(el)),
260    }
261  }
262}
263
264convert!(impl<'a> From<select::PublicChannel<'a>> for self::SupportedElement<'a> => |s| self::SupportedElement(BlockElement::from(s)));
265convert!(impl<'a> From<select::Conversation<'a>>  for self::SupportedElement<'a> => |s| self::SupportedElement(BlockElement::from(s)));
266convert!(impl<'a> From<select::User<'a>>          for self::SupportedElement<'a> => |s| self::SupportedElement(BlockElement::from(s)));
267convert!(impl<'a> From<select::External<'a>>      for self::SupportedElement<'a> => |s| self::SupportedElement(BlockElement::from(s)));
268convert!(impl<'a> From<select::Static<'a>>        for self::SupportedElement<'a> => |s| self::SupportedElement(BlockElement::from(s)));
269convert!(impl<'a> From<Button<'a>>                for self::SupportedElement<'a> => |b| self::SupportedElement(BlockElement::from(b)));
270convert!(impl<'a> From<Radio<'a>>                 for self::SupportedElement<'a> => |b| self::SupportedElement(BlockElement::from(b)));
271convert!(impl<'a> From<TextInput<'a>>             for self::SupportedElement<'a> => |t| self::SupportedElement(BlockElement::from(t)));
272convert!(impl<'a> From<DatePicker<'a>>            for self::SupportedElement<'a> => |t| self::SupportedElement(BlockElement::from(t)));
273convert!(impl<'a> From<Checkboxes<'a>>            for self::SupportedElement<'a> => |t| self::SupportedElement(BlockElement::from(t)));
274convert!(impl<'a> From<Overflow<'a>>              for self::SupportedElement<'a> => |t| self::SupportedElement(BlockElement::from(t)));