slack_messaging/blocks/
section.rs

1use crate::blocks::elements::{
2    Button, Checkboxes, DatePicker, Image, MultiSelectMenuConversations,
3    MultiSelectMenuExternalDataSource, MultiSelectMenuPublicChannels, MultiSelectMenuStaticOptions,
4    MultiSelectMenuUsers, OverflowMenu, RadioButtonGroup, SelectMenuConversations,
5    SelectMenuExternalDataSource, SelectMenuPublicChannels, SelectMenuStaticOptions,
6    SelectMenuUsers, TimePicker, WorkflowButton,
7};
8use crate::composition_objects::TextContent;
9use crate::errors::ValidationErrorKind;
10use crate::validators::*;
11
12use serde::Serialize;
13use slack_messaging_derive::Builder;
14
15/// [Section block](https://docs.slack.dev/reference/block-kit/blocks/section-block)
16/// representation.
17///
18/// # Fields and Validations
19///
20/// For more details, see the [official
21/// documentation](https://docs.slack.dev/reference/block-kit/blocks/section-block).
22///
23/// | Field | Type | Required | Validation |
24/// |-------|------|----------|------------|
25/// | text | [TextContent] | Conditionally | Minimum 1 character, Maximum 3000 characters |
26/// | block_id | String | No | Maximum 255 characters |
27/// | fields | Vec<[TextContent]> | Conditionally | Maximum 10 items, Each item maximum 2000
28/// characters |
29/// | accessory | [Accessory] | No | N/A |
30/// | expand | bool | No | N/A |
31///
32/// # Validation Across Fields
33///
34/// * Either `text` or `fields` is required. Both fields cannot be omitted.
35///
36/// # Example
37///
38/// ```
39/// use slack_messaging::{mrkdwn, plain_text};
40/// use slack_messaging::blocks::Section;
41/// use slack_messaging::blocks::elements::Image;
42/// # use std::error::Error;
43///
44/// # fn try_main() -> Result<(), Box<dyn Error>> {
45/// let section = Section::builder()
46///     .block_id("section_1")
47///     .text(mrkdwn!("A message *with some bold text* and _some italicized text_.")?)
48///     .field(mrkdwn!("High")?)
49///     .field(plain_text!("String")?)
50///     .accessory(
51///         Image::builder()
52///             .image_url("http://placekitten.com/700/500")
53///             .alt_text("Multiple cute kittens")
54///             .build()?
55///     )
56///     .build()?;
57///
58/// let expected = serde_json::json!({
59///     "type": "section",
60///     "block_id": "section_1",
61///     "text": {
62///         "type": "mrkdwn",
63///         "text": "A message *with some bold text* and _some italicized text_."
64///     },
65///     "fields": [
66///         {
67///             "type": "mrkdwn",
68///             "text": "High"
69///         },
70///         {
71///             "type": "plain_text",
72///             "text": "String"
73///         }
74///     ],
75///     "accessory": {
76///         "type": "image",
77///         "image_url": "http://placekitten.com/700/500",
78///         "alt_text": "Multiple cute kittens"
79///     }
80/// });
81///
82/// let json = serde_json::to_value(section).unwrap();
83///
84/// assert_eq!(json, expected);
85/// #     Ok(())
86/// # }
87/// # fn main() {
88/// #     try_main().unwrap()
89/// # }
90/// ```
91#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
92#[serde(tag = "type", rename = "section")]
93#[builder(validate = "validate")]
94pub struct Section {
95    #[serde(skip_serializing_if = "Option::is_none")]
96    #[builder(validate("text_object::min_1", "text_object::max_3000"))]
97    pub(crate) text: Option<TextContent>,
98
99    #[serde(skip_serializing_if = "Option::is_none")]
100    #[builder(validate("text::max_255"))]
101    pub(crate) block_id: Option<String>,
102
103    #[serde(skip_serializing_if = "Option::is_none")]
104    #[builder(
105        push_item = "field",
106        validate("list::max_item_10", "list::each_text_max_2000")
107    )]
108    pub(crate) fields: Option<Vec<TextContent>>,
109
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub(crate) accessory: Option<Accessory>,
112
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub(crate) expand: Option<bool>,
115}
116
117fn validate(val: &Section) -> Vec<ValidationErrorKind> {
118    match (val.text.as_ref(), val.fields.as_ref()) {
119        (None, None) => {
120            vec![ValidationErrorKind::EitherRequired("text", "fields")]
121        }
122        _ => vec![],
123    }
124}
125
126/// Objects that can be set to [Section] as an accessory.
127#[derive(Debug, Clone, Serialize, PartialEq)]
128#[serde(untagged)]
129pub enum Accessory {
130    /// [Button element](https://docs.slack.dev/reference/block-kit/block-elements/button-element)
131    /// representation
132    Button(Box<Button>),
133
134    /// [Checkbox group](https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element)
135    /// representation
136    Checkboxes(Box<Checkboxes>),
137
138    /// [Date picker element](https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element)
139    /// representation
140    DatePicker(Box<DatePicker>),
141
142    /// [Image element](https://docs.slack.dev/reference/block-kit/block-elements/image-element)
143    /// representation
144    Image(Box<Image>),
145
146    /// [Multi select menu of static options](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select)
147    /// representation
148    MultiSelectMenuStaticOptions(Box<MultiSelectMenuStaticOptions>),
149
150    /// [Multi select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select)
151    /// representation
152    MultiSelectMenuExternalDataSource(Box<MultiSelectMenuExternalDataSource>),
153
154    /// [Multi select menu of users](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select)
155    /// representation
156    MultiSelectMenuUsers(Box<MultiSelectMenuUsers>),
157
158    /// [Multi select menu of conversations](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#conversation_multi_select)
159    /// representation
160    MultiSelectMenuConversations(Box<MultiSelectMenuConversations>),
161
162    /// [Multi select menu of public channels](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select)
163    /// representation
164    MultiSelectMenuPublicChannels(Box<MultiSelectMenuPublicChannels>),
165
166    /// [Overflow menu element](https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element)
167    /// representation
168    OverflowMenu(Box<OverflowMenu>),
169
170    /// [Radio buton group element](https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element)
171    /// representation
172    RadioButtonGroup(Box<RadioButtonGroup>),
173
174    /// [Select menu of static options](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select)
175    /// representation
176    SelectMenuStaticOptions(Box<SelectMenuStaticOptions>),
177
178    /// [Select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select)
179    /// representation
180    SelectMenuExternalDataSource(Box<SelectMenuExternalDataSource>),
181
182    /// [Select menu of users](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select)
183    /// representation
184    SelectMenuUsers(Box<SelectMenuUsers>),
185
186    /// [Select menu of conversations](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#conversations_select)
187    /// representation
188    SelectMenuConversations(Box<SelectMenuConversations>),
189
190    /// [Select menu of public channels](https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#channels_select)
191    /// representation
192    SelectMenuPublicChannels(Box<SelectMenuPublicChannels>),
193
194    /// [Time picker element](https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element)
195    /// representation
196    TimePicker(Box<TimePicker>),
197
198    /// [Workflow button element](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element)
199    /// representation
200    WorkflowButton(Box<WorkflowButton>),
201}
202
203macro_rules! accessory_from {
204    ($($ty:ident,)*) => {
205        $(
206            impl From<$ty> for Accessory {
207                fn from(value: $ty) -> Self {
208                    Self::$ty(Box::new(value))
209                }
210            }
211         )*
212    }
213}
214
215accessory_from! {
216    Button,
217    Checkboxes,
218    DatePicker,
219    Image,
220    MultiSelectMenuStaticOptions,
221    MultiSelectMenuExternalDataSource,
222    MultiSelectMenuUsers,
223    MultiSelectMenuConversations,
224    MultiSelectMenuPublicChannels,
225    OverflowMenu,
226    RadioButtonGroup,
227    SelectMenuStaticOptions,
228    SelectMenuExternalDataSource,
229    SelectMenuUsers,
230    SelectMenuConversations,
231    SelectMenuPublicChannels,
232    TimePicker,
233    WorkflowButton,
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::blocks::elements::test_helpers::*;
240    use crate::composition_objects::test_helpers::*;
241
242    #[test]
243    fn it_implements_builder() {
244        let expected = Section {
245            text: Some(mrkdwn_text("foo").into()),
246            block_id: Some("section_0".into()),
247            fields: Some(vec![plain_text("bar").into(), mrkdwn_text("baz").into()]),
248            accessory: Some(btn("btn0", "val0").into()),
249            expand: Some(true),
250        };
251
252        let val = Section::builder()
253            .set_text(Some(mrkdwn_text("foo")))
254            .set_block_id(Some("section_0"))
255            .set_fields(Some(vec![
256                plain_text("bar").into(),
257                mrkdwn_text("baz").into(),
258            ]))
259            .set_accessory(Some(btn("btn0", "val0")))
260            .set_expand(Some(true))
261            .build()
262            .unwrap();
263
264        assert_eq!(val, expected);
265
266        let val = Section::builder()
267            .text(mrkdwn_text("foo"))
268            .block_id("section_0")
269            .fields(vec![plain_text("bar").into(), mrkdwn_text("baz").into()])
270            .accessory(btn("btn0", "val0"))
271            .expand(true)
272            .build()
273            .unwrap();
274
275        assert_eq!(val, expected);
276    }
277
278    #[test]
279    fn it_implements_push_item_method() {
280        let expected = Section {
281            text: None,
282            block_id: None,
283            fields: Some(vec![plain_text("bar").into(), mrkdwn_text("baz").into()]),
284            accessory: None,
285            expand: None,
286        };
287
288        let val = Section::builder()
289            .field(plain_text("bar"))
290            .field(mrkdwn_text("baz"))
291            .build()
292            .unwrap();
293
294        assert_eq!(val, expected);
295    }
296
297    #[test]
298    fn it_requires_text_more_than_1_character_long() {
299        let err = Section::builder()
300            .text(mrkdwn_text(""))
301            .build()
302            .unwrap_err();
303        assert_eq!(err.object(), "Section");
304
305        let errors = err.field("text");
306        assert!(errors.includes(ValidationErrorKind::MinTextLength(1)));
307    }
308
309    #[test]
310    fn it_requires_text_less_than_3000_characters_long() {
311        let err = Section::builder()
312            .text(mrkdwn_text("a".repeat(3001)))
313            .build()
314            .unwrap_err();
315        assert_eq!(err.object(), "Section");
316
317        let errors = err.field("text");
318        assert!(errors.includes(ValidationErrorKind::MaxTextLength(3000)));
319    }
320
321    #[test]
322    fn it_requires_block_id_less_than_255_characters_long() {
323        let err = Section::builder()
324            .text(mrkdwn_text("foo"))
325            .block_id("a".repeat(256))
326            .build()
327            .unwrap_err();
328        assert_eq!(err.object(), "Section");
329
330        let errors = err.field("block_id");
331        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
332    }
333
334    #[test]
335    fn it_requires_fields_list_size_less_than_10() {
336        let fields: Vec<TextContent> = (0..11).map(|_| plain_text("foobar").into()).collect();
337        let err = Section::builder().fields(fields).build().unwrap_err();
338        assert_eq!(err.object(), "Section");
339
340        let errors = err.field("fields");
341        assert!(errors.includes(ValidationErrorKind::MaxArraySize(10)));
342    }
343
344    #[test]
345    fn it_requires_each_field_text_less_than_2000_characters_long() {
346        let err = Section::builder()
347            .field(mrkdwn_text("a".repeat(2001)))
348            .build()
349            .unwrap_err();
350        assert_eq!(err.object(), "Section");
351
352        let errors = err.field("fields");
353        assert!(errors.includes(ValidationErrorKind::MaxTextLength(2000)));
354    }
355
356    #[test]
357    fn it_prevents_from_both_text_and_fields_are_not_set() {
358        let err = Section::builder().build().unwrap_err();
359        assert_eq!(err.object(), "Section");
360
361        let errors = err.across_fields();
362        assert!(errors.includes(ValidationErrorKind::EitherRequired("text", "fields")));
363    }
364}