slack_blocks/blocks/input.rs
1//! # Input Block
2//!
3//! [slack api docs 🔗]
4//!
5//! A block that collects information from users -
6//!
7//! Read [slack's guide to using modals 🔗]
8//! to learn how input blocks pass information to your app.
9//!
10//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#input
11//! [slack's guide to using modals 🔗]: https://api.slack.com/surfaces/modals/using#gathering_input
12
13use std::borrow::Cow;
14
15use serde::{Deserialize, Serialize};
16#[cfg(feature = "validation")]
17use validator::Validate;
18
19#[cfg(feature = "validation")]
20use crate::val_helpr::ValidationResult;
21use crate::{compose::text,
22 convert,
23 elems,
24 elems::{select, BlockElement}};
25
26/// # Input Block
27///
28/// [slack api docs 🔗]
29///
30/// A block that collects information from users -
31///
32/// Read [slack's guide to using modals 🔗]
33/// to learn how input blocks pass information to your app.
34///
35/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#input
36/// [slack's guide to using modals 🔗]: https://api.slack.com/surfaces/modals/using#gathering_input
37#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
38#[cfg_attr(feature = "validation", derive(Validate))]
39pub struct Input<'a> {
40 #[cfg_attr(feature = "validation", validate(custom = "validate::label"))]
41 label: text::Text,
42
43 element: SupportedElement<'a>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
46 #[cfg_attr(feature = "validation",
47 validate(custom = "super::validate_block_id"))]
48 block_id: Option<Cow<'a, str>>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
51 #[cfg_attr(feature = "validation", validate(custom = "validate::hint"))]
52 hint: Option<text::Text>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
55 dispatch_action: Option<bool>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
58 optional: Option<bool>,
59}
60
61impl<'a> Input<'a> {
62 /// Build a new input block
63 ///
64 /// For example, see `blocks::input::build::InputBuilder`.
65 pub fn builder() -> build::InputBuilderInit<'a> {
66 build::InputBuilderInit::new()
67 }
68
69 /// Validate that this Input block agrees with Slack's model requirements
70 ///
71 /// # Errors
72 /// - If `from_label_and_element` was passed a Text object longer
73 /// than 2000 chars
74 /// - If `hint` longer than 2000 chars
75 /// - If `block_id` longer than 256 chars
76 ///
77 /// # Example
78 /// ```
79 /// use slack_blocks::{blocks, elems::select};
80 ///
81 /// let select =
82 /// select::PublicChannel::builder().placeholder("Pick a channel...")
83 /// .action_id("ABC123")
84 /// .build();
85 ///
86 /// let long_string = std::iter::repeat(' ').take(2001).collect::<String>();
87 ///
88 /// let block = blocks::Input
89 /// ::builder()
90 /// .label("On a scale from 1 - 5, how angsty are you?")
91 /// .element(select)
92 /// .block_id(long_string)
93 /// .build();
94 ///
95 /// assert_eq!(true, matches!(block.validate(), Err(_)));
96 ///
97 /// // < send to slack API >
98 /// ```
99 #[cfg(feature = "validation")]
100 #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
101 pub fn validate(&self) -> ValidationResult {
102 Validate::validate(self)
103 }
104}
105
106/// Input block builder
107pub mod build {
108 use std::marker::PhantomData;
109
110 use super::*;
111 use crate::build::*;
112
113 /// Compile-time markers for builder methods
114 #[allow(non_camel_case_types)]
115 pub mod method {
116 /// InputBuilder.element
117 #[derive(Clone, Copy, Debug)]
118 pub struct element;
119
120 /// InputBuilder.label
121 #[derive(Clone, Copy, Debug)]
122 pub struct label;
123 }
124
125 /// Initial state for `InputBuilder`
126 pub type InputBuilderInit<'a> =
127 InputBuilder<'a,
128 RequiredMethodNotCalled<method::element>,
129 RequiredMethodNotCalled<method::label>>;
130
131 /// Build an Input block
132 ///
133 /// Allows you to construct safely, with compile-time checks
134 /// on required setter methods.
135 ///
136 /// # Required Methods
137 /// `InputBuilder::build()` is only available if these methods have been called:
138 /// - `element`
139 ///
140 /// # Example
141 /// ```
142 /// use slack_blocks::{blocks::Input,
143 /// compose::text::ToSlackPlaintext,
144 /// elems::TextInput};
145 ///
146 /// let block =
147 /// Input::builder().label("foo".plaintext())
148 /// .element(TextInput::builder().action_id("foo").build())
149 /// .build();
150 /// ```
151 #[derive(Debug)]
152 pub struct InputBuilder<'a, Element, Label> {
153 label: Option<text::Text>,
154 element: Option<SupportedElement<'a>>,
155 hint: Option<text::Text>,
156 block_id: Option<Cow<'a, str>>,
157 optional: Option<bool>,
158 dispatch_action: Option<bool>,
159 state: PhantomData<(Element, Label)>,
160 }
161
162 impl<'a, E, L> InputBuilder<'a, E, L> {
163 /// Create a new InputBuilder
164 pub fn new() -> Self {
165 Self { label: None,
166 element: None,
167 hint: None,
168 block_id: None,
169 optional: None,
170 dispatch_action: None,
171 state: PhantomData::<_> }
172 }
173
174 /// Set `label` (**Required**)
175 ///
176 /// A label that appears above an input element in the form of
177 /// a [text object 🔗] that must have type of `plain_text`.
178 ///
179 /// Maximum length for the text in this field is 2000 characters.
180 ///
181 /// [text object 🔗]: https://api.slack.com/reference/messaging/composition-objects#text
182 pub fn label<T>(self, label: T) -> InputBuilder<'a, E, Set<method::label>>
183 where T: Into<text::Plain>
184 {
185 InputBuilder { label: Some(label.into().into()),
186 element: self.element,
187 hint: self.hint,
188 block_id: self.block_id,
189 optional: self.optional,
190 dispatch_action: self.dispatch_action,
191 state: PhantomData::<_> }
192 }
193
194 /// Set `block_id` (Optional)
195 ///
196 /// A string acting as a unique identifier for a block.
197 ///
198 /// You can use this `block_id` when you receive an interaction payload
199 /// to [identify the source of the action 🔗].
200 ///
201 /// If not specified, a `block_id` will be generated.
202 ///
203 /// Maximum length for this field is 255 characters.
204 ///
205 /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
206 pub fn block_id<S>(mut self, block_id: S) -> Self
207 where S: Into<Cow<'a, str>>
208 {
209 self.block_id = Some(block_id.into());
210 self
211 }
212
213 /// Set `dispatch_action` (Optional)
214 ///
215 /// Will allow the elements in this block to
216 /// dispatch block_actions payloads.
217 ///
218 /// Defaults to false.
219 pub fn dispatch_actions(mut self, should: bool) -> Self {
220 self.dispatch_action = Some(should);
221 self
222 }
223
224 /// Sets `optional` (**Required**)
225 ///
226 /// A boolean that indicates whether the input
227 /// element may be empty when a user submits the modal.
228 ///
229 /// Defaults to false.
230 pub fn optional(mut self, optional: bool) -> Self {
231 self.optional = Some(optional);
232 self
233 }
234
235 /// Set `hint` (Optional)
236 ///
237 /// An optional hint that appears below an input element
238 /// in a lighter grey.
239 ///
240 /// Maximum length for the text in this field is 2000 characters.
241 pub fn hint<T>(mut self, hint: T) -> Self
242 where T: Into<text::Plain>
243 {
244 self.hint = Some(hint.into().into());
245 self
246 }
247 }
248
249 impl<'a, L> InputBuilder<'a, RequiredMethodNotCalled<method::element>, L> {
250 /// Set `element` (**Required**)
251 ///
252 /// An interactive `block_element` that will be used to gather
253 /// the input for this block.
254 ///
255 /// For the kinds of Elements supported by
256 /// Input blocks, see the `SupportedElement` enum.
257 pub fn element<El>(self,
258 element: El)
259 -> InputBuilder<'a, Set<method::element>, L>
260 where El: Into<SupportedElement<'a>>
261 {
262 InputBuilder { label: self.label,
263 element: Some(element.into()),
264 hint: self.hint,
265 block_id: self.block_id,
266 optional: self.optional,
267 dispatch_action: self.dispatch_action,
268 state: PhantomData::<_> }
269 }
270
271 /// XML child alias for `element`
272 #[cfg(feature = "blox")]
273 #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
274 pub fn child<El>(self,
275 element: El)
276 -> InputBuilder<'a, Set<method::element>, L>
277 where El: Into<SupportedElement<'a>>
278 {
279 self.element(element)
280 }
281 }
282
283 impl<'a> InputBuilder<'a, Set<method::element>, Set<method::label>> {
284 /// All done building, now give me a darn actions block!
285 ///
286 /// > `no method name 'build' found for struct 'InputBuilder<...>'`?
287 /// Make sure all required setter methods have been called. See docs for `InputBuilder`.
288 ///
289 /// ```compile_fail
290 /// use slack_blocks::blocks::Input;
291 ///
292 /// let foo = Input::builder().build(); // Won't compile!
293 /// ```
294 ///
295 /// ```
296 /// use slack_blocks::{blocks::Input,
297 /// compose::text::ToSlackPlaintext,
298 /// elems::TextInput};
299 ///
300 /// let block =
301 /// Input::builder().label("foo".plaintext())
302 /// .element(TextInput::builder().action_id("foo").build())
303 /// .build();
304 /// ```
305 pub fn build(self) -> Input<'a> {
306 Input { element: self.element.unwrap(),
307 label: self.label.unwrap(),
308 hint: self.hint,
309 dispatch_action: self.dispatch_action,
310 optional: self.optional,
311 block_id: self.block_id }
312 }
313 }
314}
315
316/// The Block Elements supported in an Input Block.
317///
318/// Supports:
319/// - Radio Buttons
320/// - Text Input
321/// - Checkboxes
322/// - Date Picker
323/// - All Select Menus
324/// - All Multi-Select Menus
325#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
326pub struct SupportedElement<'a>(BlockElement<'a>);
327
328convert!(impl<'a> From<elems::Radio<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
329convert!(impl<'a> From<elems::TextInput<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
330convert!(impl<'a> From<elems::Checkboxes<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
331convert!(impl<'a> From<elems::DatePicker<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
332
333convert!(impl<'a> From<select::Static<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
334convert!(impl<'a> From<select::External<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
335convert!(impl<'a> From<select::User<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
336convert!(impl<'a> From<select::Conversation<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
337convert!(impl<'a> From<select::PublicChannel<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
338
339convert!(impl<'a> From<select::multi::Static<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
340convert!(impl<'a> From<select::multi::External<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
341convert!(impl<'a> From<select::multi::User<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
342convert!(impl<'a> From<select::multi::Conversation<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
343convert!(impl<'a> From<select::multi::PublicChannel<'a>> for SupportedElement<'a> => |r| SupportedElement(BlockElement::from(r)));
344
345#[cfg(feature = "validation")]
346mod validate {
347 use crate::{compose::text,
348 val_helpr::{below_len, ValidatorResult}};
349
350 pub(super) fn label(text: &text::Text) -> ValidatorResult {
351 below_len("Input Label", 2000, text.as_ref())
352 }
353
354 pub(super) fn hint(text: &text::Text) -> ValidatorResult {
355 below_len("Input Hint", 2000, text.as_ref())
356 }
357}