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}