slack_messaging/
message.rs

1use crate::blocks::Block;
2use crate::validators::*;
3
4use serde::Serialize;
5use slack_messaging_derive::Builder;
6
7/// [`Message`](https://docs.slack.dev/messaging#payloads)
8/// representation.
9///
10/// See also [Header](crate::blocks::Header), [Section](crate::blocks::Section)
11/// and [any other blocks](crate::blocks) to know how to build these blocks.
12///
13/// Every block and its components have each builder and their build method
14/// returns [Result].
15///
16/// For example, according to [the official document](https://docs.slack.dev/reference/block-kit/blocks?available-in-surfaces=Home+tabs),
17/// you can include up to 50 blocks in each message. If you include more than 50
18/// blocks in a message, the build method of [MessageBuilder] returns Result::Err.
19///
20/// # Fields and Validations
21///
22/// For more details, see the [official
23/// documentation](https://docs.slack.dev/messaging#payloads).
24///
25/// | Field | Type | Required | Validation |
26/// |-------|------|----------|------------|
27/// | text | String | No | N/A |
28/// | blocks | Vec<[Block]> | No | Maximum 50 items |
29/// | thread_ts | String | No | N/A |
30/// | mrkdwn | bool | No | N/A |
31/// | response_type | String | No | N/A |
32/// | replace_original | bool | No | N/A |
33/// | delete_original | bool | No | N/A |
34/// | reply_broadcast | bool | No | N/A |
35///
36/// # Example
37///
38/// ```
39/// use slack_messaging::{mrkdwn, plain_text, Message};
40/// use slack_messaging::blocks::{Header, Section};
41/// # use std::error::Error;
42///
43/// # fn try_main() -> Result<(), Box<dyn Error>> {
44/// let message = Message::builder()
45///     .text("New Paid Time Off request from Fred Enriquez")
46///     .block(
47///         Header::builder()
48///             .text(plain_text!("New request")?)
49///             .build()?
50///     )
51///     .block(
52///         Section::builder()
53///             .field(mrkdwn!("*Type:*\nPaid Time Off")?)
54///             .field(mrkdwn!("*Created by:*\n<example.com|Fred Enriquez>")?)
55///             .build()?
56///     )
57///     .block(
58///         Section::builder()
59///             .field(mrkdwn!("*When:*\nAug 10 - Aug 13")?)
60///             .build()?
61///     )
62///     .block(
63///         Section::builder()
64///             .text(mrkdwn!("<https://example.com|View request>")?)
65///             .build()?
66///     )
67///     .build()?;
68///
69/// let expected = serde_json::json!({
70///     "text": "New Paid Time Off request from Fred Enriquez",
71///     "blocks": [
72///         {
73///             "type": "header",
74///             "text": {
75///                 "type": "plain_text",
76///                 "text": "New request"
77///             }
78///         },
79///         {
80///             "type": "section",
81///             "fields": [
82///                 {
83///                     "type": "mrkdwn",
84///                     "text": "*Type:*\nPaid Time Off"
85///                 },
86///                 {
87///                     "type": "mrkdwn",
88///                     "text": "*Created by:*\n<example.com|Fred Enriquez>"
89///                 }
90///             ]
91///         },
92///         {
93///             "type": "section",
94///             "fields": [
95///                 {
96///                     "type": "mrkdwn",
97///                     "text": "*When:*\nAug 10 - Aug 13"
98///                 }
99///             ]
100///         },
101///         {
102///             "type": "section",
103///             "text": {
104///                 "type": "mrkdwn",
105///                 "text": "<https://example.com|View request>"
106///             }
107///         }
108///     ]
109/// });
110///
111/// let json = serde_json::to_value(message)?;
112///
113/// assert_eq!(json, expected);
114/// #     Ok(())
115/// # }
116/// # fn main() {
117/// #     try_main().unwrap()
118/// # }
119/// ```
120#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
121pub struct Message {
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub(crate) text: Option<String>,
124
125    #[serde(skip_serializing_if = "Option::is_none")]
126    #[builder(push_item = "block", validate("list::max_item_50"))]
127    pub(crate) blocks: Option<Vec<Block>>,
128
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub(crate) thread_ts: Option<String>,
131
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub(crate) mrkdwn: Option<bool>,
134
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub(crate) response_type: Option<String>,
137
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub(crate) replace_original: Option<bool>,
140
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub(crate) delete_original: Option<bool>,
143
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub(crate) reply_broadcast: Option<bool>,
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::blocks::test_helpers::*;
152    use crate::errors::*;
153
154    #[test]
155    fn it_implements_builder() {
156        let expected = Message {
157            text: Some("some text".into()),
158            blocks: Some(vec![
159                header("this is a header block").into(),
160                section("this is a section block").into(),
161            ]),
162            thread_ts: Some("thread ts".into()),
163            mrkdwn: Some(true),
164            response_type: Some("response type".into()),
165            replace_original: Some(true),
166            delete_original: Some(true),
167            reply_broadcast: Some(true),
168        };
169
170        let val = Message::builder()
171            .set_text(Some("some text"))
172            .set_blocks(Some(vec![
173                header("this is a header block").into(),
174                section("this is a section block").into(),
175            ]))
176            .set_thread_ts(Some("thread ts"))
177            .set_mrkdwn(Some(true))
178            .set_response_type(Some("response type"))
179            .set_replace_original(Some(true))
180            .set_delete_original(Some(true))
181            .set_reply_broadcast(Some(true))
182            .build()
183            .unwrap();
184
185        assert_eq!(val, expected);
186
187        let val = Message::builder()
188            .text("some text")
189            .blocks(vec![
190                header("this is a header block").into(),
191                section("this is a section block").into(),
192            ])
193            .thread_ts("thread ts")
194            .mrkdwn(true)
195            .response_type("response type")
196            .replace_original(true)
197            .delete_original(true)
198            .reply_broadcast(true)
199            .build()
200            .unwrap();
201
202        assert_eq!(val, expected);
203    }
204
205    #[test]
206    fn it_impelements_push_item_method() {
207        let expected = Message {
208            text: None,
209            blocks: Some(vec![
210                header("this is a header block").into(),
211                section("this is a section block").into(),
212            ]),
213            thread_ts: None,
214            mrkdwn: None,
215            response_type: None,
216            replace_original: None,
217            delete_original: None,
218            reply_broadcast: None,
219        };
220
221        let val = Message::builder()
222            .block(header("this is a header block"))
223            .block(section("this is a section block"))
224            .build()
225            .unwrap();
226
227        assert_eq!(val, expected);
228    }
229
230    #[test]
231    fn it_requries_blocks_list_size_less_than_50() {
232        let blocks: Vec<Block> = (0..51).map(|_| section("some section").into()).collect();
233        let err = Message::builder().blocks(blocks).build().unwrap_err();
234        assert_eq!(err.object(), "Message");
235
236        let errors = err.field("blocks");
237        assert!(errors.includes(ValidationErrorKind::MaxArraySize(50)));
238    }
239}