slack_blocks/elems/
radio.rs

1//! # Radio Buttons
2//!
3//! A radio button group that allows a user to choose one item from a list of possible options.
4//!
5//! [slack api docs 🔗]
6//!
7//! Works in [blocks 🔗]: Section, Actions, Input
8//! Works in [app surfaces 🔗]: Home tabs, Modals, Messages
9//!
10//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#radio
11//! [blocks 🔗]: https://api.slack.com/reference/block-kit/blocks
12//! [app surfaces 🔗]: https://api.slack.com/surfaces
13
14use std::borrow::Cow;
15
16use serde::{Deserialize as De, Serialize as Ser};
17#[cfg(feature = "validation")]
18use validator::Validate;
19
20#[cfg(feature = "validation")]
21use crate::val_helpr::ValidationResult;
22use crate::{compose::{opt::{AnyText, NoUrl},
23                      Confirm,
24                      Opt},
25            text};
26
27/// Opt state supported by radio buttons
28pub type RadioButtonOpt<'a> = Opt<'a, AnyText, NoUrl>;
29
30/// # Radio Buttons
31///
32/// A radio button group that allows a user to choose one item from a list of possible options.
33///
34/// [slack api docs 🔗]
35///
36/// Works in [blocks 🔗]: Section, Actions, Input
37/// Works in [app surfaces 🔗]: Home tabs, Modals, Messages
38///
39/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/block-elements#radio
40/// [blocks 🔗]: https://api.slack.com/reference/block-kit/blocks
41/// [app surfaces 🔗]: https://api.slack.com/surfaces
42#[derive(Clone, Debug, Hash, PartialEq, Ser, De)]
43#[cfg_attr(feature = "validation", derive(Validate))]
44pub struct Radio<'a> {
45  #[cfg_attr(feature = "validation", validate(length(max = 255)))]
46  action_id: Cow<'a, str>, // max 255
47
48  #[cfg_attr(feature = "validation", validate(length(max = 10)))]
49  #[cfg_attr(feature = "validation", validate)]
50  options: Vec<RadioButtonOpt<'a>>, // max 10, plain or md
51
52  #[serde(skip_serializing_if = "Option::is_none")]
53  #[cfg_attr(feature = "validation", validate)]
54  initial_option: Option<RadioButtonOpt<'a>>,
55
56  #[serde(skip_serializing_if = "Option::is_none")]
57  #[cfg_attr(feature = "validation", validate)]
58  confirm: Option<Confirm>,
59}
60
61impl<'a> Radio<'a> {
62  /// Build a new Radio Button Group
63  ///
64  /// # Example
65  /// See docs for `RadioBuilder`.
66  pub fn builder() -> build::RadioBuilderInit<'a> {
67    build::RadioBuilderInit::new()
68  }
69
70  /// Validate that this select element agrees with Slack's model requirements
71  ///
72  /// # Errors
73  /// - length of `action_id` greater than 255
74  /// - length of `options` greater than 10
75  /// - one or more of `options` is invalid
76  /// - `initial_option` is set and an invalid `Opt`
77  /// - `confirm` is set and an invalid `Confirm`
78  ///
79  /// # Example
80  /// ```
81  /// use slack_blocks::{compose::Opt, elems::Radio};
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  /// let opt = Opt::builder().text_md("foo").value("bar").build();
89  ///
90  /// let opts = repeat(&opt, 11).map(|o| o.clone()).collect::<Vec<_>>();
91  ///
92  /// let input = Radio::builder().action_id(long_string)
93  ///                             .options(opts)
94  ///                             .build();
95  ///
96  /// assert!(matches!(input.validate(), Err(_)))
97  /// ```
98  #[cfg(feature = "validation")]
99  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
100  pub fn validate(&self) -> ValidationResult {
101    Validate::validate(self)
102  }
103}
104
105/// Radio button group builder
106pub mod build {
107  use std::marker::PhantomData;
108
109  use super::*;
110  use crate::build::*;
111
112  /// Required builder methods
113  #[allow(non_camel_case_types)]
114  mod method {
115    /// RadioBuilder.action_id
116    #[derive(Copy, Clone, Debug)]
117    pub struct action_id;
118    /// RadioBuilder.options
119    #[derive(Copy, Clone, Debug)]
120    pub struct options;
121  }
122
123  /// Initial state for radio button builder
124  pub type RadioBuilderInit<'a> =
125    RadioBuilder<'a,
126                 AnyText,
127                 RequiredMethodNotCalled<method::action_id>,
128                 RequiredMethodNotCalled<method::options>>;
129
130  /// Radio Button builder
131  ///
132  /// Allows you to construct a radio button safely, with compile-time checks
133  /// on required setter methods.
134  ///
135  /// # Required Methods
136  /// `RadioButton::build()` is only available if these methods have been called:
137  ///  - `options`
138  ///  - `action_id`
139  ///
140  /// # Example
141  /// ```
142  /// use slack_blocks::{blocks::{Actions, Block},
143  ///                    compose::Opt,
144  ///                    elems::Radio};
145  ///
146  /// let options = vec![Opt::builder().text_md(":joy:").value("joy").build(),
147  ///                    Opt::builder().text_md(":smirk:").value("smirk").build(),];
148  ///
149  /// let radio = Radio::builder().options(options)
150  ///                             .action_id("emoji_picker")
151  ///                             .build();
152  ///
153  /// let block: Block = Actions::builder().element(radio).build().into();
154  ///
155  /// // <send block to slack API>
156  /// ```
157  #[derive(Debug)]
158  pub struct RadioBuilder<'a, T, A, O> {
159    action_id: Option<Cow<'a, str>>,
160    options: Option<Vec<RadioButtonOpt<'a>>>,
161    initial_option: Option<RadioButtonOpt<'a>>,
162    confirm: Option<Confirm>,
163    state: PhantomData<(T, A, O)>,
164  }
165
166  impl<'a, T, A, O> RadioBuilder<'a, T, A, O> {
167    /// Construct a new RadioBuilder
168    pub fn new() -> Self {
169      Self { action_id: None,
170             options: None,
171             initial_option: None,
172             confirm: None,
173             state: PhantomData::<_> }
174    }
175
176    fn cast_state<A2, O2>(self) -> RadioBuilder<'a, T, A2, O2> {
177      RadioBuilder { action_id: self.action_id,
178                     options: self.options,
179                     initial_option: self.initial_option,
180                     confirm: self.confirm,
181                     state: PhantomData::<_> }
182    }
183
184    /// Sets `action_id` (**Required**)
185    ///
186    /// An identifier for the action triggered when the radio button group is changed.
187    ///
188    /// You can use this when you receive an interaction payload to [identify the source of the action 🔗].
189    ///
190    /// Should be unique among all other `action_id`s in the containing block.
191    /// Maximum length for this field is 255 characters.
192    ///
193    /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
194    pub fn action_id<S>(mut self,
195                        action_id: S)
196                        -> RadioBuilder<'a, T, Set<method::action_id>, O>
197      where S: Into<Cow<'a, str>>
198    {
199      self.action_id = Some(action_id.into());
200      self.cast_state()
201    }
202
203    /// Sets `options` (**Required**)
204    ///
205    /// An array of [option objects 🔗].
206    ///
207    /// A maximum of 10 options are allowed.
208    ///
209    /// [option objects 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option
210    pub fn options<I, T2: Into<text::Text>>(
211      self,
212      options: I)
213      -> RadioBuilder<'a, T2, A, Set<method::options>>
214      where I: IntoIterator<Item = Opt<'a, T2, NoUrl>>
215    {
216      let options = options.into_iter().map(|o| o.into()).collect();
217
218      RadioBuilder { action_id: self.action_id,
219                     options: Some(options),
220                     initial_option: self.initial_option,
221                     confirm: self.confirm,
222                     state: PhantomData::<_> }
223    }
224
225    /// Append an option to `options`
226    ///
227    /// A maximum of 10 options are allowed.
228    pub fn option<T2: Into<text::Text>>(
229      self,
230      opt: Opt<'a, T2, NoUrl>)
231      -> RadioBuilder<'a, T2, A, Set<method::options>> {
232      let options = match self.options {
233        | Some(mut os) => {
234          os.push(opt.into());
235          os
236        },
237        | None => vec![opt.into()],
238      };
239
240      RadioBuilder { action_id: self.action_id,
241                     options: Some(options),
242                     initial_option: self.initial_option,
243                     confirm: self.confirm,
244                     state: PhantomData::<_> }
245    }
246
247    /// Sets `initial_option` (Optional)
248    ///
249    /// An [option object 🔗] that exactly matches one of the options within `options`.
250    ///
251    /// This option will be selected when the radio button group initially loads.
252    ///
253    /// [option object 🔗]: https://api.slack.com/reference/messaging/composition-objects#option
254    pub fn initial_option<T2>(mut self, option: Opt<'a, T2, NoUrl>) -> Self
255      where T2: Into<text::Text>
256    {
257      self.initial_option = Some(option.into());
258      self
259    }
260
261    /// Allows using XML children to append options to the group.
262    #[cfg(feature = "blox")]
263    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
264    pub fn child<T2: Into<text::Text>>(
265      self,
266      opt: Opt<'a, T2, NoUrl>)
267      -> RadioBuilder<'a, T2, A, Set<method::options>> {
268      self.option(opt)
269    }
270
271    /// Sets `confirm` (Optional)
272    ///
273    /// A [confirm object 🔗] that defines an optional confirmation dialog that appears
274    /// after clicking one of the radio buttons in this element.
275    ///
276    /// [confirm object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#confirm
277    pub fn confirm(mut self, confirm: Confirm) -> Self {
278      self.confirm = Some(confirm);
279      self
280    }
281  }
282
283  impl<'a, T> RadioBuilder<'a, T, Set<method::action_id>, Set<method::options>> {
284    /// All done building, now give me a darn radio button group!
285    ///
286    /// > `no method name 'build' found for struct 'RadioBuilder<...>'`?
287    /// Make sure all required setter methods have been called. See docs for `RadioBuilder`.
288    ///
289    /// ```compile_fail
290    /// use slack_blocks::elems::Radio;
291    ///
292    /// let foo = Radio::builder().build(); // Won't compile!
293    /// ```
294    ///
295    /// ```
296    /// use slack_blocks::{compose::Opt, elems::Radio};
297    ///
298    /// let foo = Radio::builder().action_id("bar")
299    ///                           .options(vec![Opt::builder().text_md("foo")
300    ///                                                       .value("bar")
301    ///                                                       .build()])
302    ///                           .build();
303    /// ```
304    pub fn build(self) -> Radio<'a> {
305      Radio { action_id: self.action_id.unwrap(),
306              options: self.options.unwrap(),
307              initial_option: self.initial_option,
308              confirm: self.confirm }
309    }
310  }
311}