slack_blocks/compose/opt_group.rs
1//! # Option Group
2//! [slack api docs 🔗]
3//!
4//! Provides a way to group options in a [select menu 🔗] or [multi-select menu 🔗].
5//!
6//! [select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#select
7//! [multi-select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#multi_select
8//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option_group
9//! [`plain_text` only text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
10
11use serde::{Deserialize, Serialize};
12#[cfg(feature = "validation")]
13use validator::Validate;
14
15use super::{opt::{AnyText, NoUrl},
16 text,
17 Opt};
18#[cfg(feature = "validation")]
19use crate::val_helpr::ValidationResult;
20
21/// # Option Group
22/// [slack api docs 🔗]
23///
24/// Provides a way to group options in a [select menu 🔗] or [multi-select menu 🔗].
25///
26/// [select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#select
27/// [multi-select menu 🔗]: https://api.slack.com/reference/block-kit/block-elements#multi_select
28/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/composition-objects#option_group
29/// [`plain_text` only text object 🔗]: https://api.slack.com/reference/block-kit/composition-objects#text
30#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
31#[cfg_attr(feature = "validation", derive(Validate))]
32pub struct OptGroup<'a, T = AnyText, U = NoUrl> {
33 #[cfg_attr(feature = "validation", validate(custom = "validate::label"))]
34 label: text::Text,
35
36 #[cfg_attr(feature = "validation", validate(length(max = 100)))]
37 options: Vec<Opt<'a, T, U>>,
38}
39
40impl<'a> OptGroup<'a> {
41 /// Build a new option group composition object
42 ///
43 /// # Examples
44 /// see example for `OptGroupBuilder`
45 pub fn builder() -> build::OptGroupBuilderInit<'a> {
46 build::OptGroupBuilderInit::new()
47 }
48}
49
50impl<'a, T, U> OptGroup<'a, T, U> {
51 /// Validate that this Option Group object
52 /// agrees with Slack's model requirements
53 ///
54 /// # Errors
55 /// - If `label` longer than 75 chars
56 /// - If `opts` contains more than 100 options
57 ///
58 /// # Example
59 /// ```
60 /// use std::iter::repeat;
61 ///
62 /// use slack_blocks::compose::{Opt, OptGroup};
63 ///
64 /// let long_string: String = repeat(' ').take(76).collect();
65 ///
66 /// let opt = Opt::builder().text_plain("San Diego")
67 /// .value("ca_sd")
68 /// .build();
69 /// let grp = OptGroup::builder().label(long_string).option(opt).build();
70 ///
71 /// assert_eq!(true, matches!(grp.validate(), Err(_)));
72 /// ```
73 #[cfg(feature = "validation")]
74 #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
75 pub fn validate(&self) -> ValidationResult {
76 Validate::validate(self)
77 }
78}
79
80/// OptGroup builder
81pub mod build {
82 use std::marker::PhantomData;
83
84 use super::*;
85 use crate::build::*;
86
87 /// Required builder methods
88 #[allow(non_camel_case_types)]
89 mod method {
90 /// OptGroupBuilder.label
91 #[derive(Copy, Clone, Debug)]
92 pub struct label;
93 /// OptGroupBuilder.options
94 #[derive(Copy, Clone, Debug)]
95 pub struct options;
96 }
97
98 /// Option Group builder
99 ///
100 /// Allows you to construct a Option Group object safely, with compile-time checks
101 /// on required setter methods.
102 ///
103 /// # Required Methods
104 /// `OptGroup::build()` is only available if these methods have been called:
105 /// - `options`
106 /// - `label`
107 ///
108 /// # Example
109 /// ```
110 /// use std::convert::TryFrom;
111 ///
112 /// use slack_blocks::{elems::{select::Static, BlockElement},
113 /// blocks::{Actions, Block},
114 /// compose::{Opt, OptGroup}};
115 ///
116 /// #[derive(Clone, Copy, PartialEq)]
117 /// enum LangStyle {
118 /// Functional,
119 /// ObjectOriented,
120 /// SomewhereInbetween,
121 /// }
122 ///
123 /// use LangStyle::*;
124 ///
125 /// #[derive(Clone, Copy)]
126 /// struct Lang {
127 /// name: &'static str,
128 /// code: &'static str,
129 /// style: LangStyle,
130 /// }
131 ///
132 /// impl Lang {
133 /// fn new(name: &'static str, code: &'static str, style: LangStyle) -> Self {
134 /// Self {
135 /// name,
136 /// code,
137 /// style,
138 /// }
139 /// }
140 /// }
141 ///
142 /// let langs = vec![
143 /// Lang::new("Rust", "rs", SomewhereInbetween),
144 /// Lang::new("C#", "cs", ObjectOriented),
145 /// Lang::new("Haskell", "hs", Functional),
146 /// ];
147 ///
148 /// let langs_of_style = |needle: LangStyle| langs.iter()
149 /// .filter(|Lang {style, ..}| *style == needle)
150 /// .map(|lang| Opt::builder()
151 /// .text_plain(lang.name)
152 /// .value(lang.code)
153 /// .build()
154 /// )
155 /// .collect::<Vec<_>>();
156 ///
157 /// let groups = vec![
158 /// OptGroup::builder()
159 /// .label("Functional")
160 /// .options(langs_of_style(Functional))
161 /// .build(),
162 ///
163 /// OptGroup::builder()
164 /// .label("Object-Oriented")
165 /// .options(langs_of_style(ObjectOriented))
166 /// .build(),
167 ///
168 /// OptGroup::builder()
169 /// .label("Somewhere Inbetween")
170 /// .options(langs_of_style(SomewhereInbetween))
171 /// .build(),
172 /// ];
173 ///
174 /// let select =
175 /// Static::builder().placeholder("Choose your favorite programming language!")
176 /// .option_groups(groups)
177 /// .action_id("lang_chosen")
178 /// .build();
179 ///
180 /// let block: Block =
181 /// Actions::builder().element(select).build()
182 /// .into();
183 ///
184 /// // <send block to API>
185 /// ```
186 #[derive(Debug)]
187 pub struct OptGroupBuilder<'a, T, U, Options, Label> {
188 label: Option<text::Text>,
189 options: Option<Vec<Opt<'a, T, U>>>,
190 state: PhantomData<(Options, Label)>,
191 }
192
193 /// Initial state for OptGroupBuilder
194 pub type OptGroupBuilderInit<'a> =
195 OptGroupBuilder<'a,
196 AnyText,
197 NoUrl,
198 RequiredMethodNotCalled<method::options>,
199 RequiredMethodNotCalled<method::label>>;
200
201 impl<'a, T, U, O, L> OptGroupBuilder<'a, T, U, O, L> {
202 /// Construct a new OptGroupBuilder
203 pub fn new() -> Self {
204 Self { label: None,
205 options: None,
206 state: PhantomData::<_> }
207 }
208
209 fn cast_state<O2, L2>(self) -> OptGroupBuilder<'a, T, U, O2, L2> {
210 OptGroupBuilder { label: self.label,
211 options: self.options,
212 state: PhantomData::<_> }
213 }
214
215 /// Set the options of this group (**Required**, or `option`)
216 ///
217 /// An array of [option objects 🔗] that belong to
218 /// this specific group.
219 ///
220 /// Maximum of 100 items.
221 ///
222 /// [option objects 🔗]: https://api.slack.comCURRENT_PAGEoption
223 pub fn options<T2, U2, I>(
224 self,
225 options: I)
226 -> OptGroupBuilder<'a, T2, U2, Set<method::options>, L>
227 where I: IntoIterator<Item = Opt<'a, T2, U2>>
228 {
229 OptGroupBuilder { label: self.label,
230 options: Some(options.into_iter().collect()),
231 state: PhantomData::<_> }
232 }
233
234 /// A [`plain_text` only text object 🔗] that defines
235 /// the label shown above this group of options.
236 ///
237 /// Maximum length for the `text` in this field is 75 characters.
238 ///
239 /// [`plain_text` only text object 🔗]: https://api.slack.comCURRENT_PAGEtext
240 pub fn label<S>(mut self,
241 label: S)
242 -> OptGroupBuilder<'a, T, U, O, Set<method::label>>
243 where S: Into<text::Plain>
244 {
245 self.label = Some(label.into().into());
246 self.cast_state()
247 }
248 }
249
250 // First call to `option` SETS the builder's "text kind" and "whether url was set"
251 // marker type parameters
252 impl<'a, T, U, L>
253 OptGroupBuilder<'a, T, U, RequiredMethodNotCalled<method::options>, L>
254 {
255 /// Append an option to this group (**Required**, or `options`)
256 ///
257 /// Maximum of 100 items.
258 pub fn option<T2, U2>(
259 self,
260 option: Opt<'a, T2, U2>)
261 -> OptGroupBuilder<'a, T2, U2, Set<method::options>, L> {
262 OptGroupBuilder { label: self.label,
263 options: Some(vec![option.into()]),
264 state: PhantomData::<_> }
265 }
266
267 /// XML child alias for `option`.
268 #[cfg(feature = "blox")]
269 #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
270 pub fn child<T2, U2>(
271 self,
272 option: Opt<'a, T2, U2>)
273 -> OptGroupBuilder<'a, T2, U2, Set<method::options>, L> {
274 self.option::<T2, U2>(option)
275 }
276 }
277
278 // Subsequent calls must be the same type as the first call.
279 impl<'a, T, U, L> OptGroupBuilder<'a, T, U, Set<method::options>, L> {
280 /// Append an option to this group (**Required**, or `options`)
281 ///
282 /// Maximum of 100 items.
283 pub fn option(mut self, option: Opt<'a, T, U>) -> Self {
284 self.options.as_mut().unwrap().push(option);
285 self
286 }
287
288 /// XML child alias for `option`.
289 #[cfg(feature = "blox")]
290 #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
291 pub fn child(self, option: Opt<'a, T, U>) -> Self {
292 self.option(option)
293 }
294 }
295
296 impl<'a, T, U>
297 OptGroupBuilder<'a, T, U, Set<method::options>, Set<method::label>>
298 {
299 /// All done building, now give me a darn option group!
300 ///
301 /// > `no method name 'build' found for struct 'compose::opt_group::build::OptGroupBuilder<...>'`?
302 /// Make sure all required setter methods have been called. See docs for `OptGroupBuilder`.
303 ///
304 /// ```compile_fail
305 /// use slack_blocks::compose::OptGroup;
306 ///
307 /// let sel = OptGroup::builder()
308 /// .build();
309 /// /* ^^^^^ method not found in
310 /// `OptGroupBuilder<'_, RequiredMethodNotCalled<options>, RequiredMethodNotCalled<value>, _>`
311 /// */
312 /// ```
313 ///
314 /// ```
315 /// use slack_blocks::compose::{Opt, OptGroup};
316 ///
317 /// let sel = OptGroup::builder().options(vec![Opt::builder().text_plain("foo")
318 /// .value("bar")
319 /// .build()])
320 /// .label("foo")
321 /// .build();
322 /// ```
323 pub fn build(self) -> OptGroup<'a, T, U> {
324 OptGroup { label: self.label.unwrap(),
325 options: self.options.unwrap() }
326 }
327 }
328}
329
330#[cfg(feature = "validation")]
331mod validate {
332 use super::*;
333 use crate::val_helpr::{below_len, ValidatorResult};
334
335 pub(super) fn label(text: &text::Text) -> ValidatorResult {
336 below_len("Option Group Label", 75, text.as_ref())
337 }
338}