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}