Skip to main content

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            ] as Vec<TextContent>))
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![
270                plain_text("bar").into(),
271                mrkdwn_text("baz").into(),
272            ] as Vec<TextContent>)
273            .accessory(btn("btn0", "val0"))
274            .expand(true)
275            .build()
276            .unwrap();
277
278        assert_eq!(val, expected);
279    }
280
281    #[test]
282    fn it_implements_push_item_method() {
283        let expected = Section {
284            text: None,
285            block_id: None,
286            fields: Some(vec![plain_text("bar").into(), mrkdwn_text("baz").into()]),
287            accessory: None,
288            expand: None,
289        };
290
291        let val = Section::builder()
292            .field(plain_text("bar"))
293            .field(mrkdwn_text("baz"))
294            .build()
295            .unwrap();
296
297        assert_eq!(val, expected);
298    }
299
300    #[test]
301    fn it_requires_text_more_than_1_character_long() {
302        let err = Section::builder()
303            .text(mrkdwn_text(""))
304            .build()
305            .unwrap_err();
306        assert_eq!(err.object(), "Section");
307
308        let errors = err.field("text");
309        assert!(errors.includes(ValidationErrorKind::MinTextLength(1)));
310    }
311
312    #[test]
313    fn it_requires_text_less_than_3000_characters_long() {
314        let err = Section::builder()
315            .text(mrkdwn_text("a".repeat(3001)))
316            .build()
317            .unwrap_err();
318        assert_eq!(err.object(), "Section");
319
320        let errors = err.field("text");
321        assert!(errors.includes(ValidationErrorKind::MaxTextLength(3000)));
322    }
323
324    #[test]
325    fn it_requires_block_id_less_than_255_characters_long() {
326        let err = Section::builder()
327            .text(mrkdwn_text("foo"))
328            .block_id("a".repeat(256))
329            .build()
330            .unwrap_err();
331        assert_eq!(err.object(), "Section");
332
333        let errors = err.field("block_id");
334        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
335    }
336
337    #[test]
338    fn it_requires_fields_list_size_less_than_10() {
339        let fields: Vec<TextContent> = (0..11).map(|_| plain_text("foobar").into()).collect();
340        let err = Section::builder().fields(fields).build().unwrap_err();
341        assert_eq!(err.object(), "Section");
342
343        let errors = err.field("fields");
344        assert!(errors.includes(ValidationErrorKind::MaxArraySize(10)));
345    }
346
347    #[test]
348    fn it_requires_each_field_text_less_than_2000_characters_long() {
349        let err = Section::builder()
350            .field(mrkdwn_text("a".repeat(2001)))
351            .build()
352            .unwrap_err();
353        assert_eq!(err.object(), "Section");
354
355        let errors = err.field("fields");
356        assert!(errors.includes(ValidationErrorKind::MaxTextLength(2000)));
357    }
358
359    #[test]
360    fn it_prevents_from_both_text_and_fields_are_not_set() {
361        let err = Section::builder().build().unwrap_err();
362        assert_eq!(err.object(), "Section");
363
364        let errors = err.across_fields();
365        assert!(errors.includes(ValidationErrorKind::EitherRequired("text", "fields")));
366    }
367}