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}