slack_blocks/elems/
button.rs

1//! # Button
2//! [slack api docs 🔗]
3//!
4//! Works with block types:
5//! - [Section 🔗]
6//! - [Actions 🔗]
7//!
8//! An interactive component that inserts a button.
9//! The button can be a trigger for anything from opening
10//! a simple link to starting a complex workflow.
11//!
12//! To use interactive components,
13//! you will need to make some changes
14//! to prepare your app.
15//!
16//! Read our [guide to enabling interactivity 🔗].
17//!
18//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#button
19//! [Section 🔗]: https://api.slack.com/reference/block-kit/blocks#section
20//! [Actions 🔗]: https://api.slack.com/reference/block-kit/blocks#actions
21//! [guide to enabling interactivity 🔗]: https://api.slack.com/interactivity/handling
22
23use std::borrow::Cow;
24
25use serde::{Deserialize, Serialize};
26#[cfg(feature = "validation")]
27use validator::Validate;
28
29#[cfg(feature = "validation")]
30use crate::val_helpr::ValidationResult;
31use crate::{compose::Confirm, text};
32
33/// # Button
34/// [slack api docs 🔗]
35///
36/// Works with block types:
37/// - [Section 🔗]
38/// - [Actions 🔗]
39///
40/// An interactive component that inserts a button.
41/// The button can be a trigger for anything from opening
42/// a simple link to starting a complex workflow.
43///
44/// To use interactive components,
45/// you will need to make some changes
46/// to prepare your app.
47///
48/// Read our [guide to enabling interactivity 🔗].
49///
50/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#button
51/// [Section 🔗]: https://api.slack.com/reference/block-kit/blocks#section
52/// [Actions 🔗]: https://api.slack.com/reference/block-kit/blocks#actions
53/// [guide to enabling interactivity 🔗]: https://api.slack.com/interactivity/handling
54#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
55#[cfg_attr(feature = "validation", derive(Validate))]
56pub struct Button<'a> {
57  #[cfg_attr(feature = "validation", validate(custom = "validate::text"))]
58  text: text::Text,
59
60  #[cfg_attr(feature = "validation", validate(length(max = 255)))]
61  action_id: Cow<'a, str>,
62
63  #[serde(skip_serializing_if = "Option::is_none")]
64  #[cfg_attr(feature = "validation", validate(custom = "validate::url"))]
65  url: Option<Cow<'a, str>>,
66
67  #[serde(skip_serializing_if = "Option::is_none")]
68  #[cfg_attr(feature = "validation", validate(custom = "validate::value"))]
69  value: Option<Cow<'a, str>>,
70
71  #[serde(skip_serializing_if = "Option::is_none")]
72  style: Option<Style>,
73
74  #[serde(skip_serializing_if = "Option::is_none")]
75  #[cfg_attr(feature = "validation", validate)]
76  confirm: Option<Confirm>,
77}
78
79impl<'a> Button<'a> {
80  /// Build a button!
81  ///
82  /// see build::ButtonBuilder for example.
83  pub fn builder() -> build::ButtonBuilderInit<'a> {
84    build::ButtonBuilderInit::new()
85  }
86
87  /// Validate that this Button element agrees with Slack's model requirements
88  ///
89  /// # Errors
90  /// - If `action_id` is longer than 255 chars
91  /// - If `text` is longer than 75 chars
92  /// - If `url` is longer than 3000 chars
93  /// - If `value` is longer than 2000 chars
94  ///
95  /// # Example
96  /// ```
97  /// use slack_blocks::elems::Button;
98  ///
99  /// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
100  ///
101  /// let btn = Button::builder().text("Button")
102  ///                            .action_id(long_string)
103  ///                            .build();
104  ///
105  /// assert_eq!(true, matches!(btn.validate(), Err(_)));
106  /// ```
107  #[cfg(feature = "validation")]
108  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
109  pub fn validate(&self) -> ValidationResult {
110    Validate::validate(self)
111  }
112}
113
114/// Style to optionally decorate buttons with
115#[derive(Copy, Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
116#[serde(rename_all = "snake_case")]
117pub enum Style {
118  /// Gives buttons a green outline and text, ideal for affirmation or confirmation actions.
119  /// This should only be used for one button within a set.
120  Primary,
121  /// Gives buttons a red outline and text, and should be used when the action is destructive.
122  /// Use this even more sparingly than Primary.
123  Danger,
124}
125
126/// Button builder
127pub mod build {
128  use std::marker::PhantomData;
129
130  use super::*;
131  use crate::build::*;
132
133  /// Required builder methods
134  #[allow(non_camel_case_types)]
135  pub mod method {
136    /// ButtonBuilder.text
137    #[derive(Copy, Clone, Debug)]
138    pub struct text;
139
140    /// ButtonBuilder.action_id
141    #[derive(Copy, Clone, Debug)]
142    pub struct action_id;
143  }
144
145  /// Initial state for ButtonBuilder
146  pub type ButtonBuilderInit<'a> =
147    ButtonBuilder<'a,
148                  RequiredMethodNotCalled<method::text>,
149                  RequiredMethodNotCalled<method::action_id>>;
150
151  /// # Button Builder
152  ///
153  /// Allows you to construct safely, with compile-time checks
154  /// on required setter methods.
155  ///
156  /// # Required Methods
157  /// `ButtonBuilder::build()` is only available if these methods have been called:
158  ///  - `action_id`
159  ///  - `text`
160  ///
161  /// ```
162  /// use std::convert::TryFrom;
163  ///
164  /// use slack_blocks::{blocks, elems};
165  ///
166  /// let button = elems::Button::builder().text("do stuff!")
167  ///                                      .action_id("stuff")
168  ///                                      .build();
169  /// let block: blocks::Block =
170  ///   blocks::Actions::builder().element(button).build().into();
171  /// ```
172  #[derive(Debug)]
173  pub struct ButtonBuilder<'a, Text, ActionId> {
174    text: Option<text::Text>,
175    action_id: Option<Cow<'a, str>>,
176    url: Option<Cow<'a, str>>,
177    value: Option<Cow<'a, str>>,
178    style: Option<Style>,
179    confirm: Option<Confirm>,
180    state: PhantomData<(Text, ActionId)>,
181  }
182
183  impl<'a, T, A> ButtonBuilder<'a, T, A> {
184    /// Construct a new button builder
185    pub fn new() -> Self {
186      Self { text: None,
187             action_id: None,
188             url: None,
189             value: None,
190             style: None,
191             confirm: None,
192             state: PhantomData::<_> }
193    }
194
195    /// Set `style` (Optional)
196    ///
197    /// Decorates buttons with alternative visual color schemes.
198    ///
199    /// Use this option with restraint.
200    ///
201    /// If this method is not called,
202    /// the default button style will be used.
203    pub fn style(mut self, style: Style) -> Self {
204      self.style = Some(style);
205      self
206    }
207
208    /// Set `confirm` (Optional)
209    ///
210    /// A [confirm object 🔗] that defines an optional confirmation dialog after the button is clicked.
211    ///
212    /// [confirm object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#confirm
213    pub fn confirm(mut self, confirm: Confirm) -> Self {
214      self.confirm = Some(confirm);
215      self
216    }
217
218    /// Set `url` (Optional)
219    ///
220    /// A URL to load in the user's browser when the button is clicked.
221    ///
222    /// Maximum length for this field is 3000 characters.
223    ///
224    /// If you're using url, you'll still receive an interaction payload
225    /// and will need to send an acknowledgement response.
226    pub fn url(mut self, url: impl Into<Cow<'a, str>>) -> Self {
227      self.url = Some(url.into());
228      self
229    }
230
231    /// Set `value` (Optional)
232    ///
233    /// Add a meaningful value to send back to your app when this button is clicked.
234    ///
235    /// Maximum length for this field is 2000 characters.
236    pub fn value(mut self, value: impl Into<Cow<'a, str>>) -> Self {
237      self.value = Some(value.into());
238      self
239    }
240
241    /// Set `action_id` (**Required**)
242    ///
243    /// An identifier for this action.
244    ///
245    /// You can use this when you receive an interaction payload to [identify the source of the action 🔗].
246    ///
247    /// Should be unique among all other `action_id`s used elsewhere by your app.
248    ///
249    /// Maximum length for this field is 255 characters.
250    ///
251    /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
252    pub fn action_id(self,
253                     action_id: impl Into<Cow<'a, str>>)
254                     -> ButtonBuilder<'a, T, Set<method::action_id>> {
255      ButtonBuilder { text: self.text,
256                      action_id: Some(action_id.into()),
257                      url: self.url,
258                      value: self.value,
259                      style: self.style,
260                      confirm: self.confirm,
261                      state: PhantomData::<_> }
262    }
263
264    /// Alias for `text`
265    #[cfg(feature = "blox")]
266    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
267    pub fn child(self,
268                 text: impl Into<text::Plain>)
269                 -> ButtonBuilder<'a, Set<method::text>, A> {
270      self.text(text)
271    }
272
273    /// Set `text` (**Required**)
274    ///
275    /// A plain [text object 🔗] that defines the button's text.
276    ///
277    /// Maximum length for the text in this field is 75 characters.
278    ///
279    /// [text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
280    pub fn text(self,
281                text: impl Into<text::Plain>)
282                -> ButtonBuilder<'a, Set<method::text>, A> {
283      ButtonBuilder { text: Some(text.into().into()),
284                      action_id: self.action_id,
285                      url: self.url,
286                      value: self.value,
287                      style: self.style,
288                      confirm: self.confirm,
289                      state: PhantomData::<_> }
290    }
291  }
292
293  impl<'a> ButtonBuilder<'a, Set<method::text>, Set<method::action_id>> {
294    /// All done building, now give me a darn button!
295    ///
296    /// > `no method name 'build' found for struct 'ButtonBuilder<...>'`?
297    /// Make sure all required setter methods have been called. See docs for `ButtonBuilder`.
298    ///
299    /// ```compile_fail
300    /// use slack_blocks::elems::Button;
301    ///
302    /// let foo = Button::builder().build(); // Won't compile!
303    /// ```
304    ///
305    /// ```
306    /// use slack_blocks::{compose::Opt, elems::Button};
307    ///
308    /// let foo = Button::builder().action_id("foo").text("Do foo").build();
309    /// ```
310    pub fn build(self) -> Button<'a> {
311      Button { text: self.text.unwrap(),
312               action_id: self.action_id.unwrap(),
313               url: self.url,
314               confirm: self.confirm,
315               style: self.style,
316               value: self.value }
317    }
318  }
319}
320
321#[cfg(feature = "validation")]
322mod validate {
323  use super::*;
324  use crate::{text,
325              val_helpr::{below_len, ValidatorResult}};
326
327  pub(super) fn text(text: &text::Text) -> ValidatorResult {
328    below_len("Button Text", 75, text.as_ref())
329  }
330  pub(super) fn url(url: &Cow<str>) -> ValidatorResult {
331    below_len("Button.url", 3000, url)
332  }
333  pub(super) fn value(value: &Cow<str>) -> ValidatorResult {
334    below_len("Button.text", 2000, value)
335  }
336}