use crate::blocks::elements::{
Button, Checkboxes, DatePicker, Image, MultiSelectMenuConversations,
MultiSelectMenuExternalDataSource, MultiSelectMenuPublicChannels, MultiSelectMenuStaticOptions,
MultiSelectMenuUsers, OverflowMenu, RadioButtonGroup, SelectMenuConversations,
SelectMenuExternalDataSource, SelectMenuPublicChannels, SelectMenuStaticOptions,
SelectMenuUsers, TimePicker, WorkflowButton,
};
use crate::composition_objects::TextContent;
use crate::errors::ValidationErrorKind;
use crate::validators::*;
use serde::Serialize;
use slack_messaging_derive::Builder;
#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
#[serde(tag = "type", rename = "section")]
#[builder(validate = "validate")]
pub struct Section {
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(validate("text_object::min_1", "text_object::max_3000"))]
pub(crate) text: Option<TextContent>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(validate("text::max_255"))]
pub(crate) block_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[builder(
push_item = "field",
validate("list::max_item_10", "list::each_text_max_2000")
)]
pub(crate) fields: Option<Vec<TextContent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) accessory: Option<Accessory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) expand: Option<bool>,
}
fn validate(val: &Section) -> Vec<ValidationErrorKind> {
match (val.text.as_ref(), val.fields.as_ref()) {
(None, None) => {
vec![ValidationErrorKind::EitherRequired("text", "fields")]
}
_ => vec![],
}
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(untagged)]
pub enum Accessory {
Button(Box<Button>),
Checkboxes(Box<Checkboxes>),
DatePicker(Box<DatePicker>),
Image(Box<Image>),
MultiSelectMenuStaticOptions(Box<MultiSelectMenuStaticOptions>),
MultiSelectMenuExternalDataSource(Box<MultiSelectMenuExternalDataSource>),
MultiSelectMenuUsers(Box<MultiSelectMenuUsers>),
MultiSelectMenuConversations(Box<MultiSelectMenuConversations>),
MultiSelectMenuPublicChannels(Box<MultiSelectMenuPublicChannels>),
OverflowMenu(Box<OverflowMenu>),
RadioButtonGroup(Box<RadioButtonGroup>),
SelectMenuStaticOptions(Box<SelectMenuStaticOptions>),
SelectMenuExternalDataSource(Box<SelectMenuExternalDataSource>),
SelectMenuUsers(Box<SelectMenuUsers>),
SelectMenuConversations(Box<SelectMenuConversations>),
SelectMenuPublicChannels(Box<SelectMenuPublicChannels>),
TimePicker(Box<TimePicker>),
WorkflowButton(Box<WorkflowButton>),
}
macro_rules! accessory_from {
($($ty:ident,)*) => {
$(
impl From<$ty> for Accessory {
fn from(value: $ty) -> Self {
Self::$ty(Box::new(value))
}
}
)*
}
}
accessory_from! {
Button,
Checkboxes,
DatePicker,
Image,
MultiSelectMenuStaticOptions,
MultiSelectMenuExternalDataSource,
MultiSelectMenuUsers,
MultiSelectMenuConversations,
MultiSelectMenuPublicChannels,
OverflowMenu,
RadioButtonGroup,
SelectMenuStaticOptions,
SelectMenuExternalDataSource,
SelectMenuUsers,
SelectMenuConversations,
SelectMenuPublicChannels,
TimePicker,
WorkflowButton,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::blocks::elements::test_helpers::*;
use crate::composition_objects::test_helpers::*;
#[test]
fn it_implements_builder() {
let expected = Section {
text: Some(mrkdwn_text("foo").into()),
block_id: Some("section_0".into()),
fields: Some(vec![plain_text("bar").into(), mrkdwn_text("baz").into()]),
accessory: Some(btn("btn0", "val0").into()),
expand: Some(true),
};
let val = Section::builder()
.set_text(Some(mrkdwn_text("foo")))
.set_block_id(Some("section_0"))
.set_fields(Some(vec![
plain_text("bar").into(),
mrkdwn_text("baz").into(),
]))
.set_accessory(Some(btn("btn0", "val0")))
.set_expand(Some(true))
.build()
.unwrap();
assert_eq!(val, expected);
let val = Section::builder()
.text(mrkdwn_text("foo"))
.block_id("section_0")
.fields(vec![plain_text("bar").into(), mrkdwn_text("baz").into()])
.accessory(btn("btn0", "val0"))
.expand(true)
.build()
.unwrap();
assert_eq!(val, expected);
}
#[test]
fn it_implements_push_item_method() {
let expected = Section {
text: None,
block_id: None,
fields: Some(vec![plain_text("bar").into(), mrkdwn_text("baz").into()]),
accessory: None,
expand: None,
};
let val = Section::builder()
.field(plain_text("bar"))
.field(mrkdwn_text("baz"))
.build()
.unwrap();
assert_eq!(val, expected);
}
#[test]
fn it_requires_text_more_than_1_character_long() {
let err = Section::builder()
.text(mrkdwn_text(""))
.build()
.unwrap_err();
assert_eq!(err.object(), "Section");
let errors = err.field("text");
assert!(errors.includes(ValidationErrorKind::MinTextLength(1)));
}
#[test]
fn it_requires_text_less_than_3000_characters_long() {
let err = Section::builder()
.text(mrkdwn_text("a".repeat(3001)))
.build()
.unwrap_err();
assert_eq!(err.object(), "Section");
let errors = err.field("text");
assert!(errors.includes(ValidationErrorKind::MaxTextLength(3000)));
}
#[test]
fn it_requires_block_id_less_than_255_characters_long() {
let err = Section::builder()
.text(mrkdwn_text("foo"))
.block_id("a".repeat(256))
.build()
.unwrap_err();
assert_eq!(err.object(), "Section");
let errors = err.field("block_id");
assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
}
#[test]
fn it_requires_fields_list_size_less_than_10() {
let fields: Vec<TextContent> = (0..11).map(|_| plain_text("foobar").into()).collect();
let err = Section::builder().fields(fields).build().unwrap_err();
assert_eq!(err.object(), "Section");
let errors = err.field("fields");
assert!(errors.includes(ValidationErrorKind::MaxArraySize(10)));
}
#[test]
fn it_requires_each_field_text_less_than_2000_characters_long() {
let err = Section::builder()
.field(mrkdwn_text("a".repeat(2001)))
.build()
.unwrap_err();
assert_eq!(err.object(), "Section");
let errors = err.field("fields");
assert!(errors.includes(ValidationErrorKind::MaxTextLength(2000)));
}
#[test]
fn it_prevents_from_both_text_and_fields_are_not_set() {
let err = Section::builder().build().unwrap_err();
assert_eq!(err.object(), "Section");
let errors = err.across_fields();
assert!(errors.includes(ValidationErrorKind::EitherRequired("text", "fields")));
}
}