slack_blocks/elems/
overflow.rs

1//! # Overflow Menu
2//!
3//! This is like a cross between a button and a select menu -
4//! when a user clicks on this overflow button,
5//! they will be presented with a list of options to choose from.
6//!
7//! Unlike the select menu, there is no typeahead field,
8//! and the button always appears with an ellipsis ("…"),
9//! rather than customisable text.
10//!
11//! [slack api docs 🔗]
12//!
13//! Works in [blocks 🔗]: Section, Actions
14//!
15//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
16//! [blocks 🔗]: https://api.slack.com/reference/block-kit/blocks
17
18use std::borrow::Cow;
19
20use serde::{Deserialize as De, Serialize as Ser};
21#[cfg(feature = "validation")]
22use validator::Validate;
23
24#[cfg(feature = "validation")]
25use crate::val_helpr::*;
26use crate::{compose::{opt::AllowUrl, Confirm, Opt},
27            text};
28
29type MyOpt<'a> = Opt<'a, text::Plain, AllowUrl>;
30
31/// # Overflow Menu
32///
33/// This is like a cross between a button and a select menu -
34/// when a user clicks on this overflow button,
35/// they will be presented with a list of options to choose from.
36///
37/// Unlike the select menu, there is no typeahead field,
38/// and the button always appears with an ellipsis ("…"),
39/// rather than customisable text.
40///
41/// [slack api docs 🔗]
42///
43/// Works in [blocks 🔗]: Section, Actions
44///
45/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
46/// [blocks 🔗]: https://api.slack.com/reference/block-kit/blocks
47#[derive(Clone, Debug, Hash, PartialEq, Ser, De)]
48#[cfg_attr(feature = "validation", derive(Validate))]
49pub struct Overflow<'a> {
50  #[cfg_attr(feature = "validation", validate(length(max = 255)))]
51  action_id: Cow<'a, str>,
52
53  #[cfg_attr(feature = "validation", validate(length(min = 2, max = 5)))]
54  #[cfg_attr(feature = "validation", validate)]
55  options: Vec<MyOpt<'a>>,
56
57  #[cfg_attr(feature = "validation", validate)]
58  #[serde(skip_serializing_if = "Option::is_none")]
59  confirm: Option<Confirm>,
60}
61
62impl<'a> Overflow<'a> {
63  /// Construct a new Overflow Menu.
64  ///
65  /// # Example
66  /// See example of `build::OverflowBuilder`
67  pub fn builder() -> build::OverflowBuilderInit<'a> {
68    build::OverflowBuilderInit::new()
69  }
70
71  /// Validate that this select element agrees with Slack's model requirements
72  ///
73  /// # Errors
74  /// - length of `action_id` greater than 255
75  /// - length of `options` less than 2 or greater than 5
76  /// - one or more of `options` is invalid (**TODO**)
77  /// - `confirm` is set and an invalid `Confirm`
78  ///
79  /// # Example
80  /// ```
81  /// use slack_blocks::{compose::Opt, elems::Overflow};
82  ///
83  /// fn repeat<T: Copy>(el: T, n: usize) -> impl Iterator<Item = T> {
84  ///   std::iter::repeat(el).take(n)
85  /// }
86  ///
87  /// let long_string: String = repeat('a', 256).collect();
88  ///
89  /// let opt = Opt::builder().text_plain("foo")
90  ///                         .value("bar")
91  ///                         .no_url()
92  ///                         .build();
93  ///
94  /// let opts: Vec<Opt<_, _>> = repeat(&opt, 6).map(|o| o.clone()).collect();
95  ///
96  /// let input = Overflow::builder().action_id(long_string) // invalid
97  ///                                .options(opts) // also invalid
98  ///                                .build();
99  ///
100  /// assert!(matches!(input.validate(), Err(_)))
101  /// ```
102  #[cfg(feature = "validation")]
103  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
104  pub fn validate(&self) -> ValidationResult {
105    Validate::validate(self)
106  }
107}
108
109/// Overflow menu builder
110pub mod build {
111  use std::marker::PhantomData;
112
113  use super::*;
114  use crate::build::*;
115
116  /// Required builder methods
117  #[allow(non_camel_case_types)]
118  pub mod method {
119    /// OverflowBuilder.action_id
120    #[derive(Copy, Clone, Debug)]
121    pub struct action_id;
122    /// OverflowBuilder.options
123    #[derive(Copy, Clone, Debug)]
124    pub struct options;
125  }
126
127  /// Initial state for overflow builder
128  pub type OverflowBuilderInit<'a> =
129    OverflowBuilder<'a,
130                    RequiredMethodNotCalled<method::action_id>,
131                    RequiredMethodNotCalled<method::options>>;
132
133  /// Overflow Menu Builder
134  ///
135  /// Allows you to construct safely, with compile-time checks
136  /// on required setter methods.
137  ///
138  /// # Required Methods
139  /// `OverflowBuilder::build()` is only available if these methods have been called:
140  ///  - `action_id`
141  ///  - `options`
142  ///
143  /// # Example
144  /// ```
145  /// use slack_blocks::{elems::Overflow, compose::Opt};
146  ///
147  /// Overflow::builder()
148  ///          .action_id("foo")
149  ///          .options(vec![
150  ///            Opt::builder()
151  ///                .text_plain("Open in browser")
152  ///                .value("open_ext")
153  ///                .url("https://foo.org")
154  ///                .build(),
155  ///            Opt::builder()
156  ///                .text_plain("Do stuff")
157  ///                .value("do_stuff")
158  ///                .no_url()
159  ///                .build(),
160  ///          ]);
161  /// ```
162  #[derive(Debug)]
163  pub struct OverflowBuilder<'a, A, O> {
164    action_id: Option<Cow<'a, str>>,
165    options: Option<Vec<MyOpt<'a>>>,
166    confirm: Option<Confirm>,
167    state: PhantomData<(A, O)>,
168  }
169
170  impl<'a, A, O> OverflowBuilder<'a, A, O> {
171    /// Create a new empty builder
172    pub fn new() -> Self {
173      Self { action_id: None,
174             options: None,
175             confirm: None,
176             state: PhantomData::<_> }
177    }
178
179    /// Cast the internal static builder state to some other arbitrary state
180    fn cast_state<A2, O2>(self) -> OverflowBuilder<'a, A2, O2> {
181      OverflowBuilder { action_id: self.action_id,
182                        options: self.options,
183                        confirm: self.confirm,
184                        state: PhantomData::<_> }
185    }
186
187    /// Set `action_id` (**Required**)
188    ///
189    /// An identifier for the action triggered when a menu option is selected.
190    ///
191    /// You can use this when you receive an interaction payload to [identify the source of the action 🔗].
192    ///
193    /// Should be unique among all other `action_id`s in the containing block.
194    ///
195    /// Maximum length for this field is 255 characters.
196    ///
197    /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
198    pub fn action_id<T>(mut self,
199                        action_id: T)
200                        -> OverflowBuilder<'a, Set<method::action_id>, O>
201      where T: Into<Cow<'a, str>>
202    {
203      self.action_id = Some(action_id.into());
204      self.cast_state()
205    }
206
207    /// Set `options` (**Required**)
208    ///
209    /// An array of [option objects 🔗] to display in the menu.
210    ///
211    /// Maximum number of options is 5, minimum is 2.
212    ///
213    /// [option objects 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option
214    pub fn options<U>(mut self,
215                      options: Vec<Opt<'a, text::Plain, U>>)
216                      -> OverflowBuilder<'a, A, Set<method::options>> {
217      self.options =
218        Some(options.into_iter().map(|o| o.as_allow_url()).collect());
219      self.cast_state()
220    }
221
222    /// Append an option to `options`
223    ///
224    /// Maximum number of options is 5, minimum is 2.
225    pub fn option<U>(mut self,
226                     option: Opt<'a, text::Plain, U>)
227                     -> OverflowBuilder<'a, A, Set<method::options>> {
228      let options = match self.options {
229        | Some(mut options) => {
230          options.push(option.as_allow_url());
231          options
232        },
233        | None => vec![option.as_allow_url()],
234      };
235
236      self.options = Some(options);
237      self.cast_state()
238    }
239
240    /// Allows using an XML child to append an option.
241    #[cfg(feature = "blox")]
242    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
243    pub fn child<U>(self,
244                    option: Opt<'a, text::Plain, U>)
245                    -> OverflowBuilder<'a, A, Set<method::options>> {
246      self.option(option)
247    }
248
249    /// Set `confirm` (Optional)
250    ///
251    /// A [confirm object 🔗] that defines an optional confirmation dialog that appears after a menu item is selected.
252    ///
253    /// [confirm object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#confirm
254    pub fn confirm(mut self, confirm: Confirm) -> Self {
255      self.confirm = Some(confirm);
256      self
257    }
258  }
259
260  impl<'a> OverflowBuilder<'a, Set<method::action_id>, Set<method::options>> {
261    /// All done building, now give me a darn overflow menu!
262    ///
263    /// > `no method name 'build' found for struct 'OverflowBuilder<...>'`?
264    /// Make sure all required setter methods have been called. See docs for `OverflowBuilder`.
265    ///
266    /// ```compile_fail
267    /// use slack_blocks::elems::Overflow;
268    ///
269    /// let foo = Overflow::builder().build(); // Won't compile!
270    /// ```
271    ///
272    /// ```
273    /// use slack_blocks::{compose::Opt, elems::Overflow};
274    ///
275    /// let foo = Overflow::builder().action_id("bar")
276    ///                              .options(vec![Opt::builder().text_plain("foo")
277    ///                                                          .value("bar")
278    ///                                                          .no_url()
279    ///                                                          .build()])
280    ///                              .build();
281    /// ```
282    pub fn build(self) -> Overflow<'a> {
283      Overflow { action_id: self.action_id.unwrap(),
284                 options: self.options.unwrap(),
285                 confirm: self.confirm }
286    }
287  }
288}