slack_blocks/elems/select/
static_.rs

1//! # Select menu with static options
2//!
3//! [slack api docs 🔗](https://api.slack.com/reference/block-kit/block-elements#static_select)
4//!
5//! This is the simplest form of select menu,
6//! with a static list of options passed in when defining the element.
7
8use std::{borrow::Cow, marker::PhantomData};
9
10use compose::{opt::NoUrl, Confirm, Opt, OptGroup, OptOrOptGroup};
11use serde::{Deserialize, Serialize};
12#[cfg(feature = "validation")]
13use validator::Validate;
14
15#[cfg(feature = "validation")]
16use crate::val_helpr::ValidationResult;
17use crate::{compose, text};
18
19/// Opt state supported by static select
20pub type StaticOptGroup<'a> = OptGroup<'a, text::Plain, NoUrl>;
21/// Opt state supported by static select
22pub type StaticOpt<'a> = Opt<'a, text::Plain, NoUrl>;
23/// Opt state supported by static select
24pub type StaticOptOrOptGroup<'a> = OptOrOptGroup<'a, text::Plain, NoUrl>;
25
26/// # Select menu with static options
27///
28/// [slack api docs 🔗](https://api.slack.com/reference/block-kit/block-elements#static_select)
29///
30/// This is the simplest form of select menu,
31/// with a static list of options passed in when defining the element.
32#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
33#[cfg_attr(feature = "validation", derive(Validate))]
34pub struct Static<'a> {
35  #[cfg_attr(feature = "validation",
36             validate(custom = "super::validate::placeholder"))]
37  placeholder: text::Text,
38
39  #[cfg_attr(feature = "validation", validate(length(max = 255)))]
40  action_id: Cow<'a, str>,
41
42  #[serde(skip_serializing_if = "Option::is_none")]
43  #[cfg_attr(feature = "validation", validate(length(max = 100)))]
44  options: Option<Vec<StaticOpt<'a>>>,
45
46  #[serde(skip_serializing_if = "Option::is_none")]
47  #[cfg_attr(feature = "validation", validate(length(max = 100)))]
48  option_groups: Option<Vec<StaticOptGroup<'a>>>,
49
50  #[serde(skip_serializing_if = "Option::is_none")]
51  #[cfg_attr(feature = "validation", validate)]
52  confirm: Option<Confirm>,
53
54  #[serde(skip_serializing_if = "Option::is_none")]
55  initial_option: Option<StaticOptOrOptGroup<'a>>,
56}
57
58impl<'a> Static<'a> {
59  /// Build a new static select element
60  ///
61  /// # Examples
62  /// ```
63  /// use std::convert::TryFrom;
64  ///
65  /// use slack_blocks::{blocks::{Actions, Block},
66  ///                    compose::Opt,
67  ///                    elems::{select::Static, BlockElement},
68  ///                    text};
69  ///
70  /// struct City {
71  ///   name: String,
72  ///   short_code: String,
73  /// }
74  ///
75  /// impl City {
76  ///   pub fn new(name: impl ToString, short_code: impl ToString) -> Self {
77  ///     Self { name: name.to_string(),
78  ///            short_code: short_code.to_string() }
79  ///   }
80  /// }
81  ///
82  /// let cities = vec![City::new("Seattle", "SEA"),
83  ///                   City::new("Portland", "PDX"),
84  ///                   City::new("Phoenix", "PHX")];
85  ///
86  /// let options =
87  ///   cities.iter().map(|City { name, short_code }| {
88  ///                  Opt::builder().text_plain(name).value(short_code).build()
89  ///                });
90  ///
91  /// let select = Static::builder().placeholder("Choose your favorite city!")
92  ///                               .action_id("fave_city")
93  ///                               .options(options)
94  ///                               .build();
95  ///
96  /// let block: Block = Actions::builder().element(select).build().into();
97  /// ```
98  pub fn builder() -> build::StaticBuilderInit<'a> {
99    build::StaticBuilderInit::new()
100  }
101
102  /// Validate that this select element agrees with Slack's model requirements
103  ///
104  /// # Errors
105  /// - If `from_placeholder_and_action_id` was called with
106  ///     `placeholder` longer than 150 chars
107  /// - If `from_placeholder_and_action_id` was called with
108  ///     `action_id` longer than 255 chars
109  ///
110  /// # Example
111  /// ```
112  /// use slack_blocks::elems::select;
113  ///
114  /// let placeholder = r#"Hey I really would appreciate it if you chose
115  /// a channel relatively soon, so that we can figure out
116  /// where we need to send this poll, ok? it's kind of
117  /// important that you specify where this poll should be
118  /// sent, in case we haven't made that super clear.
119  /// If you understand, could you pick a channel, already??"#;
120  ///
121  /// let select = select::Static::builder().placeholder(placeholder)
122  ///                                       .action_id("abc123")
123  ///                                       .options(std::iter::empty())
124  ///                                       .build();
125  ///
126  /// assert!(matches!(select.validate(), Err(_)))
127  /// ```
128  #[cfg(feature = "validation")]
129  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
130  pub fn validate(&self) -> ValidationResult {
131    Validate::validate(self)
132  }
133}
134
135/// Static Select Builder
136pub mod build {
137  use super::*;
138  use crate::{build::*,
139              elems::select::{multi, select_kind}};
140
141  /// Required builder methods
142  #[allow(non_camel_case_types)]
143  pub mod method {
144    /// StaticBuilder.placeholder
145    #[derive(Copy, Clone, Debug)]
146    pub struct placeholder;
147
148    /// StaticBuilder.options or option_groups
149    #[derive(Copy, Clone, Debug)]
150    pub struct options;
151
152    /// StaticBuilder.action_id
153    #[derive(Copy, Clone, Debug)]
154    pub struct action_id;
155  }
156
157  /// Static Select builder
158  ///
159  /// Allows you to construct a Static Select safely, with compile-time checks
160  /// on required setter methods.
161  ///
162  /// # Required Methods
163  /// `StaticBuilder::build()` is only available if these methods have been called:
164  ///  - `placeholder`
165  ///  - `action_id`
166  ///  - `options` or `option_groups`
167  ///
168  /// # Example
169  /// ```
170  /// use std::convert::TryFrom;
171  ///
172  /// use slack_blocks::{blocks::{Actions, Block},
173  ///                    compose::Opt,
174  ///                    elems::{select::Static, BlockElement}};
175  ///
176  /// let rust = Opt::builder().text_plain("Rust").value("rs").build();
177  ///
178  /// let select =
179  ///   Static::builder().placeholder("Choose your favorite programming language!")
180  ///                    .options(vec![rust])
181  ///                    .action_id("lang_chosen")
182  ///                    .build();
183  ///
184  /// let block: Block = Actions::builder().element(select).build().into();
185  ///
186  /// // <send block to API>
187  /// ```
188  #[derive(Debug)]
189  pub struct StaticBuilder<'a, Multi, Placeholder, ActionId, Options> {
190    placeholder: Option<text::Text>,
191    action_id: Option<Cow<'a, str>>,
192    options: Option<Vec<StaticOpt<'a>>>,
193    option_groups: Option<Vec<StaticOptGroup<'a>>>,
194    confirm: Option<Confirm>,
195    initial_option: Option<StaticOptOrOptGroup<'a>>,
196    initial_options: Option<Cow<'a, [StaticOptOrOptGroup<'a>]>>,
197    max_selected_items: Option<u32>,
198    state: PhantomData<(Multi, Placeholder, ActionId, Options)>,
199  }
200
201  /// Allows the builder to statically take in "either an opt or opt group" and
202  /// enforce that the same type is used as a child later.
203  #[cfg(feature = "blox")]
204  #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
205  pub trait AppendOptOrOptGroup<'a, Multi, Placeholder, ActionId> {
206    /// The builder state after adding the opt / opt group
207    type Output;
208
209    /// Add this opt / opt group to the builder
210    fn add_to(self,
211              builder: StaticBuilder<'a,
212                            Multi,
213                            Placeholder,
214                            ActionId,
215                            RequiredMethodNotCalled<method::options>>)
216              -> Self::Output;
217  }
218
219  #[cfg(feature = "blox")]
220  #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
221  impl<'a, Multi, Placeholder, ActionId>
222    AppendOptOrOptGroup<'a, Multi, Placeholder, ActionId>
223    for Opt<'a, text::Plain, NoUrl>
224  {
225    type Output = StaticBuilder<'a,
226                                Multi,
227                                Placeholder,
228                                ActionId,
229                                Set<(method::options, StaticOpt<'a>)>>;
230    fn add_to(self,
231              builder: StaticBuilder<'a,
232                            Multi,
233                            Placeholder,
234                            ActionId,
235                            RequiredMethodNotCalled<method::options>>)
236              -> Self::Output {
237      builder.option(self)
238    }
239  }
240
241  #[cfg(feature = "blox")]
242  #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
243  impl<'a, Multi, Placeholder, ActionId>
244    AppendOptOrOptGroup<'a, Multi, Placeholder, ActionId>
245    for OptGroup<'a, text::Plain, NoUrl>
246  {
247    type Output = StaticBuilder<'a,
248                                Multi,
249                                Placeholder,
250                                ActionId,
251                                Set<(method::options, StaticOptGroup<'a>)>>;
252    fn add_to(self,
253              builder: StaticBuilder<'a,
254                            Multi,
255                            Placeholder,
256                            ActionId,
257                            RequiredMethodNotCalled<method::options>>)
258              -> Self::Output {
259      builder.option_group(self)
260    }
261  }
262
263  /// Initial state for StaticBuilder.
264  ///
265  /// Users will be able to choose one of the static options.
266  ///
267  /// To allow choosing many, use `slack_blocks::elems::select::multi::Static::builder`.
268  pub type StaticBuilderInit<'a> =
269    StaticBuilder<'a,
270                  select_kind::Single,
271                  RequiredMethodNotCalled<method::placeholder>,
272                  RequiredMethodNotCalled<method::action_id>,
273                  RequiredMethodNotCalled<method::options>>;
274
275  /// Initial state for StaticBuilder.
276  ///
277  /// Users will be able to choose many of the static options.
278  pub type MultiStaticBuilderInit<'a> =
279    StaticBuilder<'a,
280                  select_kind::Multi,
281                  RequiredMethodNotCalled<method::placeholder>,
282                  RequiredMethodNotCalled<method::action_id>,
283                  RequiredMethodNotCalled<method::options>>;
284
285  // Methods that are always available
286  impl<'a, M, P, A, O> StaticBuilder<'a, M, P, A, O> {
287    /// Construct a new StaticBuilder
288    pub fn new() -> Self {
289      Self { placeholder: None,
290             action_id: None,
291             options: None,
292             option_groups: None,
293             initial_option: None,
294             initial_options: None,
295             max_selected_items: None,
296             confirm: None,
297             state: PhantomData::<_> }
298    }
299
300    /// Change the marker type params to some other arbitrary marker type params
301    fn cast_state<P2, A2, O2>(self) -> StaticBuilder<'a, M, P2, A2, O2> {
302      StaticBuilder { placeholder: self.placeholder,
303                      action_id: self.action_id,
304                      options: self.options,
305                      option_groups: self.option_groups,
306                      confirm: self.confirm,
307                      initial_option: self.initial_option,
308                      initial_options: self.initial_options,
309                      max_selected_items: self.max_selected_items,
310                      state: PhantomData::<_> }
311    }
312
313    /// Set `placeholder` (**Required**)
314    ///
315    /// A [`plain_text` only text object 🔗] that defines
316    /// the placeholder text shown on the menu.
317    /// Maximum length for the `text` in this field is 150 characters.
318    ///
319    /// [`plain_text` only text object 🔗]: https://api.slack.comhttps://api.slack.com/reference/block-kit/composition-objects#text
320    pub fn placeholder(
321      mut self,
322      text: impl Into<text::Plain>)
323      -> StaticBuilder<'a, M, Set<method::placeholder>, A, O> {
324      self.placeholder = Some(text.into().into());
325      self.cast_state()
326    }
327
328    /// Set `action_id` (**Required**)
329    ///
330    /// An identifier for the action triggered when a menu option is selected.
331    /// You can use this when you receive an interaction payload to [identify the source of the action 🔗].
332    /// Should be unique among all other `action_id`s used elsewhere by your app.
333    /// Maximum length for this field is 255 characters.
334    ///
335    /// [identify the source of the action 🔗]: https://api.slack.comhttps://api.slack.com/interactivity/handling#payloads
336    pub fn action_id(mut self,
337                     text: impl Into<Cow<'a, str>>)
338                     -> StaticBuilder<'a, M, P, Set<method::action_id>, O> {
339      self.action_id = Some(text.into());
340      self.cast_state()
341    }
342
343    /// Set `confirm` (Optional)
344    ///
345    /// A [confirm object 🔗] that defines an
346    /// optional confirmation dialog that appears after
347    /// a menu item is selected.
348    ///
349    /// [confirm object 🔗]: https://api.slack.comhttps://api.slack.com/reference/block-kit/composition-objects#confirm
350    pub fn confirm(mut self, confirm: Confirm) -> Self {
351      self.confirm = Some(confirm);
352      self
353    }
354  }
355
356  impl<'a, P, A, O> StaticBuilder<'a, select_kind::Multi, P, A, O> {
357    /// Set `max_selected_items` (Optional)
358    ///
359    /// Specifies the maximum number of items that can be selected in the menu.
360    ///
361    /// Minimum number is 1.
362    pub fn max_selected_items(mut self, max: u32) -> Self {
363      self.max_selected_items = Some(max);
364      self
365    }
366  }
367
368  impl<'a, P, A>
369    StaticBuilder<'a,
370                  select_kind::Multi,
371                  P,
372                  A,
373                  Set<(method::options, StaticOpt<'a>)>>
374  {
375    /// Set `initial_options` (Optional)
376    ///
377    /// An array of [option objects 🔗] that exactly match one or more of the options within `options`.
378    ///
379    /// These options will be selected when the menu initially loads.
380    ///
381    /// [option objects 🔗]: https://api.slack.com/reference/messaging/composition-objects#option
382    pub fn initial_options<I>(mut self, options: I) -> Self
383      where I: IntoIterator<Item = StaticOpt<'a>>
384    {
385      self.initial_options =
386        Some(options.into_iter()
387                    .map(|o| StaticOptOrOptGroup::<'a>::Opt(o))
388                    .collect());
389      self
390    }
391  }
392
393  impl<'a, P, A>
394    StaticBuilder<'a,
395                  select_kind::Multi,
396                  P,
397                  A,
398                  Set<(method::options, StaticOpt<'a>)>>
399  {
400    /// Set `initial_options` (Optional)
401    ///
402    /// An array of [option objects 🔗] that exactly match one or more of the options within `option_groups`.
403    ///
404    /// These options will be selected when the menu initially loads.
405    ///
406    /// [option objects 🔗]: https://api.slack.com/reference/messaging/composition-objects#option
407    pub fn initial_option_groups<I>(mut self, option_groups: I) -> Self
408      where I: IntoIterator<Item = StaticOptGroup<'a>>
409    {
410      self.initial_options =
411        Some(option_groups.into_iter()
412                          .map(|o| StaticOptOrOptGroup::<'a>::OptGroup(o))
413                          .collect());
414      self
415    }
416  }
417
418  impl<'a, P, A>
419    StaticBuilder<'a,
420                  select_kind::Single,
421                  P,
422                  A,
423                  Set<(method::options, StaticOpt<'a>)>>
424  {
425    /// Set `initial_option` (Optional)
426    ///
427    /// A single option that exactly matches one of the options
428    /// that `Self::options` was called with.
429    ///
430    /// This option will be selected when the menu initially loads.
431    pub fn initial_option(mut self, option: StaticOpt<'a>) -> Self {
432      self.initial_option = Some(StaticOptOrOptGroup::<'a>::Opt(option));
433      self
434    }
435  }
436
437  impl<'a, P, A>
438    StaticBuilder<'a,
439                  select_kind::Single,
440                  P,
441                  A,
442                  Set<(method::options, StaticOptGroup<'a>)>>
443  {
444    /// Set `initial_option` (Optional)
445    ///
446    /// A single option group that exactly matches one of the option groups
447    /// that `Self::options` was called with.
448    ///
449    /// This option will be selected when the menu initially loads.
450    pub fn initial_option_group(mut self,
451                                option_group: StaticOptGroup<'a>)
452                                -> Self {
453      self.initial_option =
454        Some(StaticOptOrOptGroup::<'a>::OptGroup(option_group));
455      self
456    }
457  }
458
459  impl<'a, M, P, A>
460    StaticBuilder<'a, M, P, A, RequiredMethodNotCalled<method::options>>
461  {
462    /// Append an Opt or OptGroup as a child XML element.
463    ///
464    /// The type signature trickery here infers whether you've passed
465    /// an Opt or OptGroup, and will ensure the following children will
466    /// be the same type.
467    #[cfg(feature = "blox")]
468    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
469    pub fn child<Opt>(self, child: Opt) -> Opt::Output
470      where Opt: AppendOptOrOptGroup<'a, M, P, A>
471    {
472      child.add_to(self)
473    }
474
475    /// Set `options` (this or `Self::option_groups` is **Required**)
476    ///
477    /// An array of [option objects 🔗].
478    /// Maximum number of options is 100.
479    ///
480    /// [option objects 🔗]: https://api.slack.comhttps://api.slack.com/reference/block-kit/composition-objects#option
481    pub fn options<Iter>(
482      mut self,
483      options: Iter)
484      -> StaticBuilder<'a, M, P, A, Set<(method::options, StaticOpt<'a>)>>
485      where Iter: IntoIterator<Item = StaticOpt<'a>>
486    {
487      self.options = Some(options.into_iter().collect::<Vec<_>>());
488      self.cast_state()
489    }
490
491    /// Append an option to `options`
492    ///
493    /// Maximum number of options is 100.
494    pub fn option(
495      mut self,
496      option: impl Into<StaticOpt<'a>>)
497      -> StaticBuilder<'a, M, P, A, Set<(method::options, StaticOpt<'a>)>> {
498      self.options = Some(vec![option.into()]);
499      self.cast_state()
500    }
501
502    /// Append an option_group to `option_groups`
503    ///
504    /// Maximum number of option groups is 100.
505    pub fn option_group(
506      mut self,
507      option: impl Into<StaticOptGroup<'a>>)
508      -> StaticBuilder<'a, M, P, A, Set<(method::options, StaticOptGroup<'a>)>>
509    {
510      self.option_groups = Some(vec![option.into()]);
511      self.cast_state()
512    }
513
514    /// Set `option_groups` (this or `Self::options` is **Required**)
515    ///
516    /// An array of [option group objects 🔗].
517    /// Maximum number of option groups is 100.
518    ///
519    /// [option group objects 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option_group
520    pub fn option_groups<Iter>(
521      mut self,
522      groups: Iter)
523      -> StaticBuilder<'a, M, P, A, Set<(method::options, StaticOptGroup<'a>)>>
524      where Iter: IntoIterator<Item = StaticOptGroup<'a>>
525    {
526      self.option_groups = Some(groups.into_iter().collect::<Vec<_>>());
527      self.cast_state()
528    }
529  }
530
531  impl<'a, M, P, A>
532    StaticBuilder<'a, M, P, A, Set<(method::options, StaticOpt<'a>)>>
533  {
534    /// Append an option to `options`
535    ///
536    /// Maximum number of options is 100.
537    pub fn option(mut self, option: impl Into<StaticOpt<'a>>) -> Self {
538      self.options.as_mut().unwrap().push(option.into());
539      self
540    }
541
542    /// Append an Opt as a child XML element.
543    #[cfg(feature = "blox")]
544    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
545    pub fn child(self, option: impl Into<StaticOpt<'a>>) -> Self {
546      self.option(option)
547    }
548  }
549
550  impl<'a, M, P, A>
551    StaticBuilder<'a, M, P, A, Set<(method::options, StaticOptGroup<'a>)>>
552  {
553    /// Append an option_group to `option_groups`
554    ///
555    /// Maximum number of option groups is 100.
556    pub fn option_group(mut self,
557                        option: impl Into<StaticOptGroup<'a>>)
558                        -> Self {
559      self.option_groups.as_mut().unwrap().push(option.into());
560      self.cast_state()
561    }
562
563    /// Append an OptGroup as a child XML element.
564    #[cfg(feature = "blox")]
565    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
566    pub fn child(self, option: impl Into<StaticOptGroup<'a>>) -> Self {
567      self.option_group(option)
568    }
569  }
570
571  impl<'a, O>
572    StaticBuilder<'a,
573                  select_kind::Single,
574                  Set<method::placeholder>,
575                  Set<method::action_id>,
576                  Set<O>>
577  {
578    /// All done building, now give me a darn select element!
579    ///
580    /// > `no method name 'build' found for struct 'select::static_::build::StaticBuilder<...>'`?
581    /// Make sure all required setter methods have been called. See docs for `StaticBuilder`.
582    ///
583    /// ```compile_fail
584    /// use slack_blocks::elems::select::Static;
585    ///
586    /// let sel = Static::builder().build(); // Won't compile!
587    /// ```
588    ///
589    /// ```
590    /// use slack_blocks::elems::select::Static;
591    ///
592    /// let sel = Static::builder().placeholder("foo")
593    ///                            .action_id("bar")
594    ///                            .options(vec![])
595    ///                            .build();
596    /// ```
597    pub fn build(self) -> Static<'a> {
598      Static { placeholder: self.placeholder.unwrap(),
599               action_id: self.action_id.unwrap(),
600               options: self.options,
601               option_groups: self.option_groups,
602               confirm: self.confirm,
603               initial_option: self.initial_option }
604    }
605  }
606
607  impl<'a, O>
608    StaticBuilder<'a,
609                  select_kind::Multi,
610                  Set<method::placeholder>,
611                  Set<method::action_id>,
612                  Set<O>>
613  {
614    /// All done building, now give me a darn select element!
615    ///
616    /// > `no method name 'build' found for struct 'select::static_::build::StaticBuilder<...>'`?
617    /// Make sure all required setter methods have been called. See docs for `StaticBuilder`.
618    ///
619    /// ```compile_fail
620    /// use slack_blocks::elems::select::Static;
621    ///
622    /// let sel = Static::builder().build(); // Won't compile!
623    /// ```
624    ///
625    /// ```
626    /// use slack_blocks::elems::select::Static;
627    ///
628    /// let sel = Static::builder().placeholder("foo")
629    ///                            .action_id("bar")
630    ///                            .options(vec![])
631    ///                            .build();
632    /// ```
633    pub fn build(self) -> multi::Static<'a> {
634      multi::Static { placeholder: self.placeholder.unwrap(),
635                      action_id: self.action_id.unwrap(),
636                      options: self.options,
637                      option_groups: self.option_groups,
638                      confirm: self.confirm,
639                      initial_options: self.initial_options,
640                      max_selected_items: self.max_selected_items }
641    }
642  }
643}