slack_blocks/compose/
opt_group.rs

1//! # Option Group
2//! [slack api docs 🔗]
3//!
4//! Provides a way to group options in a [select menu 🔗] or [multi-select menu 🔗].
5//!
6//! [select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#select
7//! [multi-select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#multi_select
8//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option_group
9//! [`plain_text` only text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
10
11use serde::{Deserialize, Serialize};
12#[cfg(feature = "validation")]
13use validator::Validate;
14
15use super::{opt::{AnyText, NoUrl},
16            text,
17            Opt};
18#[cfg(feature = "validation")]
19use crate::val_helpr::ValidationResult;
20
21/// # Option Group
22/// [slack api docs 🔗]
23///
24/// Provides a way to group options in a [select menu 🔗] or [multi-select menu 🔗].
25///
26/// [select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#select
27/// [multi-select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#multi_select
28/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option_group
29/// [`plain_text` only text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
30#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
31#[cfg_attr(feature = "validation", derive(Validate))]
32pub struct OptGroup<'a, T = AnyText, U = NoUrl> {
33  #[cfg_attr(feature = "validation", validate(custom = "validate::label"))]
34  label: text::Text,
35
36  #[cfg_attr(feature = "validation", validate(length(max = 100)))]
37  options: Vec<Opt<'a, T, U>>,
38}
39
40impl<'a> OptGroup<'a> {
41  /// Build a new option group composition object
42  ///
43  /// # Examples
44  /// see example for `OptGroupBuilder`
45  pub fn builder() -> build::OptGroupBuilderInit<'a> {
46    build::OptGroupBuilderInit::new()
47  }
48}
49
50impl<'a, T, U> OptGroup<'a, T, U> {
51  /// Validate that this Option Group object
52  /// agrees with Slack's model requirements
53  ///
54  /// # Errors
55  /// - If `label` longer than 75 chars
56  /// - If `opts` contains more than 100 options
57  ///
58  /// # Example
59  /// ```
60  /// use std::iter::repeat;
61  ///
62  /// use slack_blocks::compose::{Opt, OptGroup};
63  ///
64  /// let long_string: String = repeat(' ').take(76).collect();
65  ///
66  /// let opt = Opt::builder().text_plain("San Diego")
67  ///                         .value("ca_sd")
68  ///                         .build();
69  /// let grp = OptGroup::builder().label(long_string).option(opt).build();
70  ///
71  /// assert_eq!(true, matches!(grp.validate(), Err(_)));
72  /// ```
73  #[cfg(feature = "validation")]
74  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
75  pub fn validate(&self) -> ValidationResult {
76    Validate::validate(self)
77  }
78}
79
80/// OptGroup builder
81pub mod build {
82  use std::marker::PhantomData;
83
84  use super::*;
85  use crate::build::*;
86
87  /// Required builder methods
88  #[allow(non_camel_case_types)]
89  mod method {
90    /// OptGroupBuilder.label
91    #[derive(Copy, Clone, Debug)]
92    pub struct label;
93    /// OptGroupBuilder.options
94    #[derive(Copy, Clone, Debug)]
95    pub struct options;
96  }
97
98  ///  Option Group builder
99  ///
100  ///  Allows you to construct a Option Group object safely, with compile-time checks
101  ///  on required setter methods.
102  ///
103  ///  # Required Methods
104  ///  `OptGroup::build()` is only available if these methods have been called:
105  ///   - `options`
106  ///   - `label`
107  ///
108  ///  # Example
109  ///  ```
110  ///  use std::convert::TryFrom;
111  ///
112  ///  use slack_blocks::{elems::{select::Static, BlockElement},
113  ///                     blocks::{Actions, Block},
114  ///                     compose::{Opt, OptGroup}};
115  ///
116  ///  #[derive(Clone, Copy, PartialEq)]
117  ///  enum LangStyle {
118  ///    Functional,
119  ///    ObjectOriented,
120  ///    SomewhereInbetween,
121  ///  }
122  ///
123  ///  use LangStyle::*;
124  ///
125  ///  #[derive(Clone, Copy)]
126  ///  struct Lang {
127  ///    name: &'static str,
128  ///    code: &'static str,
129  ///    style: LangStyle,
130  ///  }
131  ///
132  ///  impl Lang {
133  ///    fn new(name: &'static str, code: &'static str, style: LangStyle) -> Self {
134  ///      Self {
135  ///        name,
136  ///        code,
137  ///        style,
138  ///      }
139  ///    }
140  ///  }
141  ///
142  ///  let langs = vec![
143  ///    Lang::new("Rust", "rs", SomewhereInbetween),
144  ///    Lang::new("C#", "cs", ObjectOriented),
145  ///    Lang::new("Haskell", "hs", Functional),
146  ///  ];
147  ///
148  ///  let langs_of_style = |needle: LangStyle| langs.iter()
149  ///                                                .filter(|Lang {style, ..}| *style == needle)
150  ///                                                .map(|lang| Opt::builder()
151  ///                                                                .text_plain(lang.name)
152  ///                                                                .value(lang.code)
153  ///                                                                .build()
154  ///                                                )
155  ///                                                .collect::<Vec<_>>();
156  ///
157  ///  let groups = vec![
158  ///    OptGroup::builder()
159  ///             .label("Functional")
160  ///             .options(langs_of_style(Functional))
161  ///             .build(),
162  ///
163  ///    OptGroup::builder()
164  ///             .label("Object-Oriented")
165  ///             .options(langs_of_style(ObjectOriented))
166  ///             .build(),
167  ///
168  ///    OptGroup::builder()
169  ///             .label("Somewhere Inbetween")
170  ///             .options(langs_of_style(SomewhereInbetween))
171  ///             .build(),
172  ///  ];
173  ///
174  ///  let select =
175  ///    Static::builder().placeholder("Choose your favorite programming language!")
176  ///                     .option_groups(groups)
177  ///                     .action_id("lang_chosen")
178  ///                     .build();
179  ///
180  ///  let block: Block =
181  ///    Actions::builder().element(select).build()
182  ///                             .into();
183  ///
184  ///  // <send block to API>
185  ///  ```
186  #[derive(Debug)]
187  pub struct OptGroupBuilder<'a, T, U, Options, Label> {
188    label: Option<text::Text>,
189    options: Option<Vec<Opt<'a, T, U>>>,
190    state: PhantomData<(Options, Label)>,
191  }
192
193  /// Initial state for OptGroupBuilder
194  pub type OptGroupBuilderInit<'a> =
195    OptGroupBuilder<'a,
196                    AnyText,
197                    NoUrl,
198                    RequiredMethodNotCalled<method::options>,
199                    RequiredMethodNotCalled<method::label>>;
200
201  impl<'a, T, U, O, L> OptGroupBuilder<'a, T, U, O, L> {
202    /// Construct a new OptGroupBuilder
203    pub fn new() -> Self {
204      Self { label: None,
205             options: None,
206             state: PhantomData::<_> }
207    }
208
209    fn cast_state<O2, L2>(self) -> OptGroupBuilder<'a, T, U, O2, L2> {
210      OptGroupBuilder { label: self.label,
211                        options: self.options,
212                        state: PhantomData::<_> }
213    }
214
215    /// Set the options of this group (**Required**, or `option`)
216    ///
217    /// An array of [option objects 🔗] that belong to
218    /// this specific group.
219    ///
220    /// Maximum of 100 items.
221    ///
222    /// [option objects 🔗]: https://api.slack.comCURRENT_PAGEoption
223    pub fn options<T2, U2, I>(
224      self,
225      options: I)
226      -> OptGroupBuilder<'a, T2, U2, Set<method::options>, L>
227      where I: IntoIterator<Item = Opt<'a, T2, U2>>
228    {
229      OptGroupBuilder { label: self.label,
230                        options: Some(options.into_iter().collect()),
231                        state: PhantomData::<_> }
232    }
233
234    /// A [`plain_text` only text object 🔗] that defines
235    /// the label shown above this group of options.
236    ///
237    /// Maximum length for the `text` in this field is 75 characters.
238    ///
239    /// [`plain_text` only text object 🔗]: https://api.slack.comCURRENT_PAGEtext
240    pub fn label<S>(mut self,
241                    label: S)
242                    -> OptGroupBuilder<'a, T, U, O, Set<method::label>>
243      where S: Into<text::Plain>
244    {
245      self.label = Some(label.into().into());
246      self.cast_state()
247    }
248  }
249
250  // First call to `option` SETS the builder's "text kind" and "whether url was set"
251  // marker type parameters
252  impl<'a, T, U, L>
253    OptGroupBuilder<'a, T, U, RequiredMethodNotCalled<method::options>, L>
254  {
255    /// Append an option to this group (**Required**, or `options`)
256    ///
257    /// Maximum of 100 items.
258    pub fn option<T2, U2>(
259      self,
260      option: Opt<'a, T2, U2>)
261      -> OptGroupBuilder<'a, T2, U2, Set<method::options>, L> {
262      OptGroupBuilder { label: self.label,
263                        options: Some(vec![option.into()]),
264                        state: PhantomData::<_> }
265    }
266
267    /// XML child alias for `option`.
268    #[cfg(feature = "blox")]
269    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
270    pub fn child<T2, U2>(
271      self,
272      option: Opt<'a, T2, U2>)
273      -> OptGroupBuilder<'a, T2, U2, Set<method::options>, L> {
274      self.option::<T2, U2>(option)
275    }
276  }
277
278  // Subsequent calls must be the same type as the first call.
279  impl<'a, T, U, L> OptGroupBuilder<'a, T, U, Set<method::options>, L> {
280    /// Append an option to this group (**Required**, or `options`)
281    ///
282    /// Maximum of 100 items.
283    pub fn option(mut self, option: Opt<'a, T, U>) -> Self {
284      self.options.as_mut().unwrap().push(option);
285      self
286    }
287
288    /// XML child alias for `option`.
289    #[cfg(feature = "blox")]
290    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
291    pub fn child(self, option: Opt<'a, T, U>) -> Self {
292      self.option(option)
293    }
294  }
295
296  impl<'a, T, U>
297    OptGroupBuilder<'a, T, U, Set<method::options>, Set<method::label>>
298  {
299    /// All done building, now give me a darn option group!
300    ///
301    /// > `no method name 'build' found for struct 'compose::opt_group::build::OptGroupBuilder<...>'`?
302    /// Make sure all required setter methods have been called. See docs for `OptGroupBuilder`.
303    ///
304    /// ```compile_fail
305    /// use slack_blocks::compose::OptGroup;
306    ///
307    /// let sel = OptGroup::builder()
308    ///                    .build();
309    /// /*                  ^^^^^ method not found in
310    ///                    `OptGroupBuilder<'_, RequiredMethodNotCalled<options>, RequiredMethodNotCalled<value>, _>`
311    /// */
312    /// ```
313    ///
314    /// ```
315    /// use slack_blocks::compose::{Opt, OptGroup};
316    ///
317    /// let sel = OptGroup::builder().options(vec![Opt::builder().text_plain("foo")
318    ///                                                          .value("bar")
319    ///                                                          .build()])
320    ///                              .label("foo")
321    ///                              .build();
322    /// ```
323    pub fn build(self) -> OptGroup<'a, T, U> {
324      OptGroup { label: self.label.unwrap(),
325                 options: self.options.unwrap() }
326    }
327  }
328}
329
330#[cfg(feature = "validation")]
331mod validate {
332  use super::*;
333  use crate::val_helpr::{below_len, ValidatorResult};
334
335  pub(super) fn label(text: &text::Text) -> ValidatorResult {
336    below_len("Option Group Label", 75, text.as_ref())
337  }
338}