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}