Skip to main content

slack_messaging/blocks/rich_text/
mod.rs

1use crate::validators::*;
2
3use paste::paste;
4use serde::Serialize;
5use slack_messaging_derive::Builder;
6
7/// Builder objects for [rich text sub-elements](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block/#usage-info).
8pub mod builders;
9
10/// Objects representing [rich text element types](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block/#element-types).
11pub mod types;
12
13/// Re-export of all rich text sub-elements and types.
14pub mod prelude {
15    pub use super::types::*;
16    pub use super::{
17        ListStyle,
18        RichText,
19        RichTextList,
20        RichTextPreformatted,
21        RichTextQuote,
22        RichTextSection,
23        RichTextSubElement,
24    };
25}
26
27mod list;
28mod preformatted;
29mod quote;
30mod section;
31
32pub use list::{ListStyle, RichTextList};
33pub use preformatted::RichTextPreformatted;
34pub use quote::RichTextQuote;
35pub use section::RichTextSection;
36
37/// [Rich text sub elements](https://api.slack.com/reference/block-kit/blocks#element-types)
38/// representation.
39#[derive(Debug, Clone, Serialize, PartialEq)]
40#[serde(untagged)]
41pub enum RichTextSubElement {
42    /// [Rich text section element](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block#rich_text_section)
43    Section(Box<RichTextSection>),
44
45    /// [Rich text list element](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block#rich_text_list)
46    List(Box<RichTextList>),
47
48    /// [Rich text preformatted element](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block#rich_text_preformatted)
49    Preformatted(Box<RichTextPreformatted>),
50
51    /// [Rich text quote element](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block#rich_text_quote)
52    Quote(Box<RichTextQuote>),
53}
54
55macro_rules! impl_sub_element {
56    ($($var:tt,)*) => {
57        paste! {
58            $(
59                impl From<[<RichText $var>]> for RichTextSubElement {
60                    fn from(value: [<RichText $var>]) -> Self {
61                        Self::$var(Box::new(value))
62                    }
63                }
64            )*
65        }
66    };
67}
68
69impl_sub_element! {
70    Section,
71    List,
72    Preformatted,
73    Quote,
74}
75
76/// [Rich text block](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block) representation.
77///
78/// # Fields and Validations
79///
80/// For more details, see the [official
81/// documentation](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block).
82///
83/// | Field | Type | Required | Validation |
84/// |-------|------|----------|------------|
85/// | elements | Vec<[RichTextSubElement]> | Yes | N/A |
86/// | block_id | String | No | Max length 255 characters |
87///
88/// # Example
89///
90/// ```
91/// use slack_messaging::blocks::rich_text::prelude::*;
92/// # use std::error::Error;
93///
94/// # fn try_main() -> Result<(), Box<dyn Error>> {
95/// let rich_text = RichText::builder()
96///     .block_id("rich-text-block-0")
97///     .element(
98///         RichTextSection::builder()
99///             .element(
100///                 RichTextElementText::builder()
101///                     .text("Hello there, ")
102///                     .build()?
103///             )
104///             .element(
105///                 RichTextElementText::builder()
106///                     .text("I am a bold rich text block!")
107///                     .style(
108///                         RichTextStyle::builder()
109///                             .bold(true)
110///                             .build()?
111///                     )
112///                     .build()?
113///             )
114///             .build()?
115///     )
116///     .build()?;
117///
118/// let expected = serde_json::json!({
119///     "type": "rich_text",
120///     "block_id": "rich-text-block-0",
121///     "elements": [
122///         {
123///             "type": "rich_text_section",
124///             "elements": [
125///                 {
126///                     "type": "text",
127///                     "text": "Hello there, "
128///                 },
129///                 {
130///                     "type": "text",
131///                     "text": "I am a bold rich text block!",
132///                     "style": {
133///                         "bold": true
134///                     }
135///                 }
136///             ]
137///         }
138///     ]
139/// });
140///
141/// let json = serde_json::to_value(rich_text).unwrap();
142///
143/// assert_eq!(json, expected);
144///
145/// // If your object has any validation errors, the build method returns Result::Err
146/// let rich_text = RichText::builder().build();
147/// assert!(rich_text.is_err());
148/// #     Ok(())
149/// # }
150/// # fn main() {
151/// #     try_main().unwrap()
152/// # }
153/// ```
154#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
155#[serde(tag = "type", rename = "rich_text")]
156pub struct RichText {
157    #[builder(push_item = "element", validate("required"))]
158    pub(crate) elements: Option<Vec<RichTextSubElement>>,
159
160    #[serde(skip_serializing_if = "Option::is_none")]
161    #[builder(validate("text::max_255"))]
162    pub(crate) block_id: Option<String>,
163}
164
165#[cfg(test)]
166mod tests {
167    use super::test_helpers::*;
168    use super::types::test_helpers::*;
169    use super::*;
170    use crate::errors::*;
171
172    #[test]
173    fn it_implements_builder() {
174        let expected = RichText {
175            block_id: Some("rich_text_0".into()),
176            elements: Some(vec![section(vec![el_text("foo"), el_emoji("var")]).into()]),
177        };
178
179        let val = RichText::builder()
180            .set_block_id(Some("rich_text_0"))
181            .set_elements(Some(vec![
182                section(vec![el_text("foo"), el_emoji("var")]),
183            ]))
184            .build()
185            .unwrap();
186
187        assert_eq!(val, expected);
188
189        let val = RichText::builder()
190            .block_id("rich_text_0")
191            .elements(vec![section(vec![el_text("foo"), el_emoji("var")])])
192            .build()
193            .unwrap();
194
195        assert_eq!(val, expected);
196    }
197
198    #[test]
199    fn it_implements_push_item_method() {
200        let expected = RichText {
201            block_id: None,
202            elements: Some(vec![
203                section(vec![el_text("foo"), el_emoji("var")]).into(),
204                section(vec![el_text("baz")]).into(),
205            ]),
206        };
207
208        let val = RichText::builder()
209            .element(section(vec![el_text("foo"), el_emoji("var")]))
210            .element(section(vec![el_text("baz")]))
211            .build()
212            .unwrap();
213
214        assert_eq!(val, expected);
215    }
216
217    #[test]
218    fn it_requires_elements_field() {
219        let err = RichText::builder().build().unwrap_err();
220        assert_eq!(err.object(), "RichText");
221
222        let errors = err.field("elements");
223        assert!(errors.includes(ValidationErrorKind::Required));
224    }
225
226    #[test]
227    fn it_requires_block_id_less_than_255_characters_long() {
228        let err = RichText::builder()
229            .block_id("a".repeat(256))
230            .element(section(vec![el_text("baz")]))
231            .build()
232            .unwrap_err();
233        assert_eq!(err.object(), "RichText");
234
235        let errors = err.field("block_id");
236        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
237    }
238}
239
240#[cfg(test)]
241pub mod test_helpers {
242    use super::types::RichTextElementType;
243    use super::*;
244
245    pub fn section(elements: Vec<RichTextElementType>) -> RichTextSection {
246        RichTextSection {
247            elements: Some(elements),
248        }
249    }
250}