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#[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#[derive(Debug, Clone, Serialize, PartialEq)]
128#[serde(untagged)]
129pub enum Accessory {
130 Button(Box<Button>),
133
134 Checkboxes(Box<Checkboxes>),
137
138 DatePicker(Box<DatePicker>),
141
142 Image(Box<Image>),
145
146 MultiSelectMenuStaticOptions(Box<MultiSelectMenuStaticOptions>),
149
150 MultiSelectMenuExternalDataSource(Box<MultiSelectMenuExternalDataSource>),
153
154 MultiSelectMenuUsers(Box<MultiSelectMenuUsers>),
157
158 MultiSelectMenuConversations(Box<MultiSelectMenuConversations>),
161
162 MultiSelectMenuPublicChannels(Box<MultiSelectMenuPublicChannels>),
165
166 OverflowMenu(Box<OverflowMenu>),
169
170 RadioButtonGroup(Box<RadioButtonGroup>),
173
174 SelectMenuStaticOptions(Box<SelectMenuStaticOptions>),
177
178 SelectMenuExternalDataSource(Box<SelectMenuExternalDataSource>),
181
182 SelectMenuUsers(Box<SelectMenuUsers>),
185
186 SelectMenuConversations(Box<SelectMenuConversations>),
189
190 SelectMenuPublicChannels(Box<SelectMenuPublicChannels>),
193
194 TimePicker(Box<TimePicker>),
197
198 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}