slack_blocks/compose/
opt.rs

1//! # Option Object
2//! [slack api docs 🔗]
3//!
4//! An object that represents a single selectable item in a
5//! - [select menu 🔗],
6//! - [multi-select menu 🔗],
7//! - [checkbox group 🔗],
8//! - [radio button group 🔗],
9//! - or [overflow menu 🔗].
10//!
11//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option
12//! [select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#select
13//! [multi-select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#multi_select
14//! [checkbox group 🔗]: https://api.slack.com/reference/block-kit/block-elements#checkboxes
15//! [radio button group 🔗]: https://api.slack.com/reference/block-kit/block-elements#radio
16//! [overflow menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
17
18use std::{borrow::Cow, marker::PhantomData};
19
20use serde::{Deserialize, Serialize};
21#[cfg(feature = "validation")]
22use validator::Validate;
23
24use super::text;
25#[cfg(feature = "validation")]
26use crate::val_helpr::ValidationResult;
27use crate::{build::*, convert};
28
29/// Opt supports text::Plain and text::Mrkdwn.
30#[derive(Copy, Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
31#[cfg_attr(feature = "validation", derive(Validate))]
32pub struct AnyText;
33
34/// Opt does not support urls.
35#[derive(Copy, Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
36#[cfg_attr(feature = "validation", derive(Validate))]
37pub struct NoUrl;
38
39/// Opt does support urls.
40#[derive(Copy, Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
41#[cfg_attr(feature = "validation", derive(Validate))]
42pub struct AllowUrl;
43
44convert!(impl From<NoUrl> for AllowUrl => |_| AllowUrl);
45
46/// # Option Object
47/// [slack api docs 🔗]
48///
49/// An object that represents a single selectable item in a
50/// - [select menu 🔗],
51/// - [multi-select menu 🔗],
52/// - [checkbox group 🔗],
53/// - [radio button group 🔗],
54/// - or [overflow menu 🔗].
55///
56/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option
57/// [select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#select
58/// [multi-select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#multi_select
59/// [checkbox group 🔗]: https://api.slack.com/reference/block-kit/block-elements#checkboxes
60/// [radio button group 🔗]: https://api.slack.com/reference/block-kit/block-elements#radio
61/// [overflow menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
62#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
63#[cfg_attr(feature = "validation", derive(Validate))]
64pub struct Opt<'a, T = AnyText, U = NoUrl> {
65  #[cfg_attr(feature = "validation", validate(custom = "validate::text"))]
66  text: text::Text,
67
68  #[cfg_attr(feature = "validation", validate(length(max = 75)))]
69  value: Cow<'a, str>,
70
71  #[cfg_attr(feature = "validation", validate(custom = "validate::desc"))]
72  #[serde(skip_serializing_if = "Option::is_none")]
73  description: Option<text::Text>,
74
75  #[cfg_attr(feature = "validation", validate(custom = "validate::url"))]
76  #[serde(skip_serializing_if = "Option::is_none")]
77  url: Option<Cow<'a, str>>,
78
79  #[serde(skip)]
80  marker: PhantomData<(T, U)>,
81}
82
83impl<'a, T: Into<text::Text>, U> From<Opt<'a, T, U>> for Opt<'a, AnyText, U> {
84  fn from(o: Opt<'a, T, U>) -> Self {
85    Opt { text: o.text,
86          value: o.value,
87          description: o.description,
88          url: o.url,
89          marker: PhantomData::<(AnyText, U)> }
90  }
91}
92
93// Constructor functions
94impl<'a> Opt<'a> {
95  /// Build a new option composition object
96  ///
97  /// # Examples
98  /// ```
99  /// use slack_blocks::{blocks::{Actions, Block},
100  ///                    compose::Opt,
101  ///                    elems::{select::Static, BlockElement},
102  ///                    text};
103  ///
104  /// struct City {
105  ///   name: String,
106  ///   short_code: String,
107  /// }
108  ///
109  /// impl City {
110  ///   pub fn new(name: impl ToString, short_code: impl ToString) -> Self {
111  ///     Self { name: name.to_string(),
112  ///            short_code: short_code.to_string() }
113  ///   }
114  /// }
115  ///
116  /// let cities = vec![City::new("Seattle", "SEA"),
117  ///                   City::new("Portland", "PDX"),
118  ///                   City::new("Phoenix", "PHX")];
119  ///
120  /// let options =
121  ///   cities.iter().map(|City { name, short_code }| {
122  ///                  Opt::builder().text_plain(name).value(short_code).build()
123  ///                });
124  ///
125  /// let select = Static::builder().placeholder("Choose your favorite city!")
126  ///                               .action_id("fave_city")
127  ///                               .options(options)
128  ///                               .build();
129  ///
130  /// let block: Block = Actions::builder().element(select).build().into();
131  /// ```
132  pub fn builder() -> build::OptBuilderInit<'a> {
133    build::OptBuilderInit::new()
134  }
135}
136
137impl<'a, U> Opt<'a, text::Plain, U> {
138  /// Ensure the type flags of the opt say "AllowUrl", used to mix NoUrl and AllowUrl in overflow menus.
139  pub(crate) fn as_allow_url(self) -> Opt<'a, text::Plain, AllowUrl> {
140    Opt { text: self.text,
141          value: self.value,
142          description: self.description,
143          url: self.url,
144          marker: PhantomData::<(text::Plain, AllowUrl)> }
145  }
146}
147
148// Methods available to all specializations
149impl<'a, T, U> Opt<'a, T, U> {
150  /// Validate that this Option composition object
151  /// agrees with Slack's model requirements
152  ///
153  /// # Errors
154  /// - If `text` longer than 75 chars
155  /// - If `value` longer than 75 chars
156  /// - If `url` longer than 3000 chars
157  /// - If `description` longer than 75 chars
158  ///
159  /// # Example
160  /// ```
161  /// use std::iter::repeat;
162  ///
163  /// use slack_blocks::compose::Opt;
164  ///
165  /// let long_string: String = repeat(' ').take(76).collect();
166  ///
167  /// let opt = Opt::builder().text_plain("My Option")
168  ///                         .value(long_string)
169  ///                         .build();
170  ///
171  /// assert_eq!(true, matches!(opt.validate(), Err(_)));
172  /// ```
173  #[cfg(feature = "validation")]
174  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
175  pub fn validate(&self) -> ValidationResult {
176    Validate::validate(self)
177  }
178}
179
180/// Opt builder
181pub mod build {
182  use std::marker::PhantomData;
183
184  use super::*;
185
186  /// Required builder methods
187  #[allow(non_camel_case_types)]
188  pub mod method {
189    /// OptBuilder.value
190    #[derive(Copy, Clone, Debug)]
191    pub struct value;
192    /// OptBuilder.text
193    #[derive(Copy, Clone, Debug)]
194    pub struct text;
195    /// OptBuilder.url
196    #[derive(Copy, Clone, Debug)]
197    pub struct url;
198  }
199
200  /// Initial state for OptBuilder
201  pub type OptBuilderInit<'a> =
202    OptBuilder<'a,
203               RequiredMethodNotCalled<method::text>,
204               RequiredMethodNotCalled<method::value>,
205               OptionalMethodNotCalled<method::url>>;
206
207  /// Option builder
208  ///
209  /// Allows you to construct a Option composition object safely, with compile-time checks
210  /// on required setter methods.
211  ///
212  /// # Required Methods
213  /// `Opt::build()` is only available if these methods have been called:
214  ///  - `text` or `text_plain` or `text_md`
215  ///  - `value`
216  ///
217  /// # Example
218  /// ```
219  /// use std::convert::TryFrom;
220  ///
221  /// use slack_blocks::{blocks::{Actions, Block},
222  ///                    compose::Opt,
223  ///                    elems::{select::Static, BlockElement}};
224  /// let langs = vec![("Rust", "rs"), ("Haskell", "hs"), ("NodeJS", "node")];
225  ///
226  /// let langs =
227  ///   langs.into_iter().map(|(name, code)| {
228  ///                      Opt::builder().text_plain(name).value(code).build()
229  ///                    });
230  ///
231  /// let select =
232  ///   Static::builder().placeholder("Choose your favorite programming language!")
233  ///                    .options(langs)
234  ///                    .action_id("lang_chosen")
235  ///                    .build();
236  ///
237  /// let block: Block = Actions::builder().element(select).build().into();
238  ///
239  /// // <send block to API>
240  /// ```
241  #[derive(Debug)]
242  pub struct OptBuilder<'a, Text, Value, Url> {
243    text: Option<text::Text>,
244    value: Option<Cow<'a, str>>,
245    description: Option<text::Text>,
246    url: Option<Cow<'a, str>>,
247    state: PhantomData<(Text, Value, Url)>,
248  }
249
250  impl<T, V, U> OptBuilder<'static, T, V, U> {
251    /// Construct a new OptBuilder
252    pub fn new() -> OptBuilderInit<'static> {
253      OptBuilderInit { text: None,
254                       value: None,
255                       description: None,
256                       url: None,
257                       state: PhantomData::<_> }
258    }
259  }
260
261  impl<'a, T, V, U> OptBuilder<'a, T, V, U> {
262    /// Change the marker type params to some other arbitrary marker type params
263    fn cast_state<T2, V2, U2>(self) -> OptBuilder<'a, T2, V2, U2> {
264      OptBuilder { text: self.text,
265                   value: self.value,
266                   description: self.description,
267                   url: self.url,
268                   state: PhantomData::<_> }
269    }
270
271    /// Set `value` (**Required**)
272    ///
273    /// The string value that will be passed to your app
274    /// when this option is chosen.
275    ///
276    /// Maximum length for this field is 75 characters.
277    pub fn value<S>(mut self,
278                    value: S)
279                    -> OptBuilder<'a, T, Set<method::value>, U>
280      where S: Into<Cow<'a, str>>
281    {
282      self.value = Some(value.into());
283      self.cast_state()
284    }
285
286    /// Set `description` (Optional)
287    ///
288    /// A [`plain_text` only text object 🔗] that defines
289    /// a line of descriptive text shown below the `text` field
290    /// beside the radio button.
291    ///
292    /// Maximum length for the `text` object within this field
293    /// is 75 characters.
294    ///
295    /// [`plain_text` only text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
296    pub fn desc<S>(mut self, desc: S) -> OptBuilder<'a, T, V, U>
297      where S: Into<text::Plain>
298    {
299      self.description = Some(desc.into().into());
300      self.cast_state()
301    }
302  }
303
304  impl<'a, V, U> OptBuilder<'a, RequiredMethodNotCalled<method::text>, V, U> {
305    /// Alias for `text`, allowing you to set the text of the option like so:
306    /// ```
307    /// use slack_blocks::{blox::*, compose::Opt, text};
308    ///
309    /// let xml = blox! {
310    ///   <option value="foo">
311    ///     <text kind=plain>"Foo"</text>
312    ///   </option>
313    /// };
314    ///
315    /// let equiv = Opt::builder().value("foo")
316    ///                           .text(text::Plain::from("Foo"))
317    ///                           .build();
318    ///
319    /// assert_eq!(xml, equiv)
320    /// ```
321    #[cfg(feature = "blox")]
322    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
323    pub fn child<T: Into<text::Text>>(
324      self,
325      text: T)
326      -> OptBuilder<'a, Set<(method::text, T)>, V, U> {
327      self.text(text)
328    }
329
330    /// Set `text` (**Required**)
331    ///
332    /// A [text object 🔗] that defines the text shown in the option on the menu.
333    /// Overflow, select, and multi-select menus
334    /// can only use `plain_text` objects,
335    /// while radio buttons and checkboxes
336    /// can use `mrkdwn` text objects.
337    ///
338    /// Maximum length for the `text` in this field is 75 characters.
339    ///
340    /// [text object 🔗]: https://api.slack.com#text
341    pub fn text<Txt>(mut self,
342                     text: Txt)
343                     -> OptBuilder<'a, Set<(method::text, Txt)>, V, U>
344      where Txt: Into<text::Text>
345    {
346      self.text = Some(text.into());
347      self.cast_state()
348    }
349
350    /// Set `text` (**Required**)
351    ///
352    /// A [text object 🔗] that defines the text shown in the option on the menu.
353    /// Overflow, select, and multi-select menus
354    /// can only use `plain_text` objects,
355    /// while radio buttons and checkboxes
356    /// can use `mrkdwn` text objects.
357    ///
358    /// Maximum length for the `text` in this field is 75 characters.
359    ///
360    /// [text object 🔗]: https://api.slack.com#text
361    pub fn text_plain<Txt>(
362      self,
363      text: Txt)
364      -> OptBuilder<'a, Set<(method::text, text::Plain)>, V, U>
365      where Txt: Into<text::Plain>
366    {
367      self.text(text.into())
368    }
369
370    /// Set `text` (**Required**)
371    ///
372    /// A [text object 🔗] that defines the text shown in the option on the menu.
373    /// Overflow, select, and multi-select menus
374    /// can only use `plain_text` objects,
375    /// while radio buttons and checkboxes
376    /// can use `mrkdwn` text objects.
377    ///
378    /// Maximum length for the `text` in this field is 75 characters.
379    ///
380    /// [text object 🔗]: https://api.slack.com#text
381    pub fn text_md<Txt>(
382      self,
383      text: Txt)
384      -> OptBuilder<'a, Set<(method::text, text::Mrkdwn)>, V, U>
385      where Txt: Into<text::Mrkdwn>
386    {
387      self.text(text.into())
388    }
389  }
390
391  impl<'a, V, U> OptBuilder<'a, Set<(method::text, text::Plain)>, V, U> {
392    /// Set `url` (Optional)
393    ///
394    /// The URL will be loaded in the user's browser when the option is clicked.
395    ///
396    /// Maximum length for this field is 3000 characters.
397    ///
398    /// The `url` attribute is only available in [overflow menus 🔗]
399    ///
400    /// If you're using `url`, you'll still receive an [interaction payload 🔗]
401    /// and will need to [send an acknowledgement response 🔗].
402    ///
403    /// [overflow menus 🔗]: https://api.slack.com/reference/block-kit/block-elements#overflow
404    /// [interaction payload 🔗]: https://api.slack.com/interactivity/handling#payloads
405    /// [send an acknowledgement response 🔗]: https://api.slack.com/interactivity/handling#acknowledgment_response
406    pub fn url<S>(
407      mut self,
408      url: S)
409      -> OptBuilder<'a, Set<(method::text, text::Plain)>, V, Set<method::url>>
410      where S: Into<Cow<'a, str>>
411    {
412      self.url = Some(url.into());
413      self.cast_state()
414    }
415
416    /// Flag opt as being usable in an `AllowUrl` context without setting Url explicitly.
417    pub fn no_url(
418      self)
419      -> OptBuilder<'a, Set<(method::text, text::Plain)>, V, Set<method::url>>
420    {
421      self.cast_state()
422    }
423  }
424
425  impl<'a>
426    OptBuilder<'a,
427               Set<(method::text, text::Plain)>,
428               Set<method::value>,
429               Set<method::url>>
430  {
431    /// All done building, now give me a darn option!
432    ///
433    /// > `no method name 'build' found for struct 'compose::opt::build::OptBuilder<...>'`?
434    ///
435    /// Make sure all required setter methods have been called. See docs for `OptBuilder`.
436    ///
437    /// ```compile_fail
438    /// use slack_blocks::compose::Opt;
439    ///
440    /// let sel = Opt::builder().build(); // Won't compile!
441    /// ```
442    ///
443    /// ```
444    /// use slack_blocks::compose::Opt;
445    ///
446    /// let opt = Opt::builder().text_plain("cheese")
447    ///                         .value("cheese")
448    ///                         .url("https://cheese.com")
449    ///                         .build();
450    /// ```
451    pub fn build(self) -> Opt<'a, text::Plain, AllowUrl> {
452      Opt { text: self.text.unwrap(),
453            value: self.value.unwrap(),
454            url: self.url,
455            description: self.description,
456            marker: PhantomData::<_> }
457    }
458  }
459
460  impl<'a, T: Into<text::Text>>
461    OptBuilder<'a,
462               Set<(method::text, T)>,
463               Set<method::value>,
464               OptionalMethodNotCalled<method::url>>
465  {
466    /// All done building, now give me a darn option!
467    ///
468    /// > `no method name 'build' found for struct 'compose::opt::build::OptBuilder<...>'`?
469    ///
470    /// Make sure all required setter methods have been called. See docs for `OptBuilder`.
471    ///
472    /// ```compile_fail
473    /// use slack_blocks::compose::Opt;
474    ///
475    /// let sel = Opt::builder().text_plain("foo")
476    ///                         .build();
477    /// /*                       ^^^^^ method not found in
478    ///                          `OptBuilder<'_, Set<(text, text::Plain)>, RequiredMethodNotCalled<value>>`
479    /// */
480    /// ```
481    ///
482    /// ```
483    /// use slack_blocks::compose::Opt;
484    ///
485    /// let opt = Opt::builder().text_md("cheese").value("cheese").build();
486    /// ```
487    pub fn build(self) -> Opt<'a, T, NoUrl> {
488      Opt { text: self.text.unwrap(),
489            value: self.value.unwrap(),
490            url: self.url,
491            description: self.description,
492            marker: PhantomData::<_> }
493    }
494  }
495}
496
497#[cfg(feature = "validation")]
498mod validate {
499  use super::*;
500  use crate::val_helpr::{below_len, ValidatorResult};
501
502  pub(super) fn text(text: &text::Text) -> ValidatorResult {
503    below_len("Option Text", 75, text.as_ref())
504  }
505
506  pub(super) fn desc(text: &text::Text) -> ValidatorResult {
507    below_len("Option Description", 75, text.as_ref())
508  }
509
510  pub(super) fn url(text: &Cow<'_, str>) -> ValidatorResult {
511    below_len("URL", 3000, text.as_ref())
512  }
513}