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)));