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 ] 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}