slack_messaging/composition_objects/
option_group.rs

1use crate::composition_objects::{Opt, Plain, Text, TextExt};
2use crate::validators::*;
3
4use serde::Serialize;
5use slack_messaging_derive::Builder;
6
7/// [Option group object](https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object)
8/// representation.
9///
10/// This is a generic struct that can represent an option group object with different text object
11/// types.
12///
13/// # Type Parameters
14///
15/// * `T`: The type of text object used for the `text` field of the [`Opt`] objects in the
16///   `options` field. Defaults to `Text<Plain>`. Must implement the [`TextExt`] trait.
17///
18/// # Fields and Validations
19///
20/// For more details, see the [official
21/// documentation](https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object).
22///
23/// | Field | Type | Required | Validation |
24/// |-------|------|----------|------------|
25/// | label | [Text]<[Plain]> | Yes | Max length 75 characters |
26/// | options | Vec<[Opt]<`T`>> | Yes | Must contain at most 100 items |
27///
28/// # Example
29///
30/// ```
31/// use slack_messaging::plain_text;
32/// use slack_messaging::composition_objects::{OptGroup, Opt, Plain, Text};
33/// # use std::error::Error;
34///
35/// # fn try_main() -> Result<(), Box<dyn Error>> {
36/// let options: OptGroup = OptGroup::builder()
37///     .label(plain_text!("Group One")?)
38///     .option(
39///         Opt::builder()
40///             .text(plain_text!("option-0")?)
41///             .value("value-0")
42///             .build()?
43///     )
44///     .option(
45///         Opt::builder()
46///             .text(plain_text!("option-1")?)
47///             .value("value-1")
48///             .build()?
49///     )
50///     .build()?;
51///
52/// let expected = serde_json::json!({
53///     "label": {
54///         "type": "plain_text",
55///         "text": "Group One"
56///     },
57///     "options": [
58///         {
59///             "text": {
60///                 "type": "plain_text",
61///                 "text": "option-0",
62///             },
63///             "value": "value-0"
64///         },
65///         {
66///             "text": {
67///                 "type": "plain_text",
68///                 "text": "option-1"
69///             },
70///             "value": "value-1"
71///         },
72///     ]
73/// });
74///
75/// let json = serde_json::to_value(options).unwrap();
76///
77/// assert_eq!(json, expected);
78///
79/// // If your object has any validation errors, the build method returns Result::Err
80/// let options = OptGroup::<Text<Plain>>::builder()
81///     .label(plain_text!("Group One")?)
82///     .build();
83///
84/// assert!(options.is_err());
85/// #     Ok(())
86/// # }
87/// # fn main() {
88/// #     try_main().unwrap()
89/// # }
90/// ```
91#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
92#[serde(bound(serialize = "T: Serialize"))]
93pub struct OptGroup<T = Text<Plain>>
94where
95    T: TextExt,
96{
97    #[builder(validate("required", "text_object::max_75"))]
98    pub(crate) label: Option<Text<Plain>>,
99
100    #[builder(push_item = "option", validate("required", "list::max_item_100"))]
101    pub(crate) options: Option<Vec<Opt<T>>>,
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::composition_objects::test_helpers::*;
108    use crate::errors::*;
109
110    #[test]
111    fn it_implements_builder() {
112        let expected = OptGroup {
113            label: Some(plain_text("foo")),
114            options: Some(vec![
115                option("text_0", "value_0"),
116                option("text_1", "value_1"),
117            ]),
118        };
119
120        let val = OptGroup::builder()
121            .set_label(Some(plain_text("foo")))
122            .set_options(Some(vec![
123                option("text_0", "value_0"),
124                option("text_1", "value_1"),
125            ]))
126            .build()
127            .unwrap();
128
129        assert_eq!(val, expected);
130
131        let val = OptGroup::builder()
132            .label(plain_text("foo"))
133            .options(vec![
134                option("text_0", "value_0"),
135                option("text_1", "value_1"),
136            ])
137            .build()
138            .unwrap();
139
140        assert_eq!(val, expected);
141    }
142
143    #[test]
144    fn it_implements_push_item_method() {
145        let expected = OptGroup {
146            label: Some(plain_text("foo")),
147            options: Some(vec![
148                option("text_0", "value_0"),
149                option("text_1", "value_1"),
150            ]),
151        };
152
153        let val = OptGroup::builder()
154            .label(plain_text("foo"))
155            .option(option("text_0", "value_0"))
156            .option(option("text_1", "value_1"))
157            .build()
158            .unwrap();
159
160        assert_eq!(val, expected);
161    }
162
163    #[test]
164    fn it_requires_label_field() {
165        let err = OptGroup::builder()
166            .options(vec![
167                option("text_0", "value_0"),
168                option("text_1", "value_1"),
169            ])
170            .build()
171            .unwrap_err();
172        assert_eq!(err.object(), "OptGroup");
173
174        let errors = err.field("label");
175        assert!(errors.includes(ValidationErrorKind::Required));
176    }
177
178    #[test]
179    fn it_requires_label_less_than_75_characters_long() {
180        let err = OptGroup::builder()
181            .label(plain_text("a".repeat(76)))
182            .options(vec![
183                option("text_0", "value_0"),
184                option("text_1", "value_1"),
185            ])
186            .build()
187            .unwrap_err();
188        assert_eq!(err.object(), "OptGroup");
189
190        let errors = err.field("label");
191        assert!(errors.includes(ValidationErrorKind::MaxTextLength(75)));
192    }
193
194    #[test]
195    fn it_requires_options_field() {
196        let err = OptGroup::<Text<Plain>>::builder()
197            .label(plain_text("foo"))
198            .build()
199            .unwrap_err();
200        assert_eq!(err.object(), "OptGroup");
201
202        let errors = err.field("options");
203        assert!(errors.includes(ValidationErrorKind::Required));
204    }
205
206    #[test]
207    fn it_requires_options_size_less_than_100() {
208        let options: Vec<Opt> = (0..101).map(|_| option("opt", "val")).collect();
209        let err = OptGroup::builder()
210            .label(plain_text("foo"))
211            .options(options)
212            .build()
213            .unwrap_err();
214        assert_eq!(err.object(), "OptGroup");
215
216        let errors = err.field("options");
217        assert!(errors.includes(ValidationErrorKind::MaxArraySize(100)));
218    }
219}