Skip to main content

slack_messaging/blocks/
plan.rs

1use crate::blocks::TaskCard;
2use crate::validators::*;
3
4use serde::Serialize;
5use slack_messaging_derive::Builder;
6
7/// [Plan block](https://docs.slack.dev/reference/block-kit/blocks/plan-block) representation.
8///
9/// # Fields and Validations
10///
11/// For more details, see the [official documentation](https://docs.slack.dev/reference/block-kit/blocks/plan-block).
12///
13/// | Field | Type | Required | Validation |
14/// |-------|------|----------|------------|
15/// | title | String | Yes | N/A |
16/// | block_id | String | No | Maximum 255 characters |
17/// | tasks | Vec<[TaskCard]> | No | N/A |
18///
19/// # Example
20///
21/// ```
22/// use slack_messaging::blocks::{Plan, RichText, TaskCard, TaskStatus};
23/// use slack_messaging::blocks::rich_text::RichTextSection;
24/// use slack_messaging::blocks::rich_text::types::RichTextElementText;
25/// # use std::error::Error;
26///
27/// # fn try_main() -> Result<(), Box<dyn Error>> {
28/// let plan = Plan::builder()
29///     .title("Thinking completed")
30///     .task(
31///         TaskCard::builder()
32///             .task_id("call_001")
33///             .title("Fetched user profile information")
34///             .status(TaskStatus::InProgress)
35///             .details(
36///                 RichText::builder()
37///                     .block_id("viMWO")
38///                     .element(
39///                         RichTextSection::builder()
40///                             .element(
41///                                 RichTextElementText::builder()
42///                                     .text("Searched database...")
43///                                     .build()?
44///                             )
45///                             .build()?
46///                     )
47///                     .build()?
48///             )
49///             .output(
50///                 RichText::builder()
51///                     .block_id("viMWO")
52///                     .element(
53///                         RichTextSection::builder()
54///                             .element(
55///                                 RichTextElementText::builder()
56///                                     .text("Profile data loaded")
57///                                     .build()?
58///                             )
59///                             .build()?
60///                     )
61///                     .build()?
62///             )
63///             .build()?
64///     )
65///     .task(
66///         TaskCard::builder()
67///             .task_id("call_002")
68///             .title("Checked user permissions")
69///             .status(TaskStatus::Pending)
70///             .build()?
71///     )
72///     .task(
73///         TaskCard::builder()
74///             .task_id("call_003")
75///             .title("Generated comprehensive user report")
76///             .status(TaskStatus::Complete)
77///             .output(
78///                 RichText::builder()
79///                     .block_id("crsk")
80///                     .element(
81///                         RichTextSection::builder()
82///                             .element(
83///                                 RichTextElementText::builder()
84///                                     .text("15 data points compiled")
85///                                     .build()?
86///                             )
87///                             .build()?
88///                     )
89///                     .build()?
90///             )
91///             .build()?
92///     )
93///     .build()?;
94///
95/// let expected = serde_json::json!({
96///   "type": "plan",
97///   "title": "Thinking completed",
98///   "tasks": [
99///     {
100///       "type": "task_card",
101///       "task_id": "call_001",
102///       "title": "Fetched user profile information",
103///       "status": "in_progress",
104///       "details": {
105///         "type": "rich_text",
106///         "block_id": "viMWO",
107///         "elements": [
108///           {
109///             "type": "rich_text_section",
110///             "elements": [
111///               {
112///                 "type": "text",
113///                 "text": "Searched database..."
114///               }
115///             ]
116///           }
117///         ]
118///       },
119///       "output": {
120///         "type": "rich_text",
121///         "block_id": "viMWO",
122///         "elements": [
123///           {
124///             "type": "rich_text_section",
125///             "elements": [
126///               {
127///                 "type": "text",
128///                 "text": "Profile data loaded"
129///               }
130///             ]
131///           }
132///         ]
133///       }
134///     },
135///     {
136///       "type": "task_card",
137///       "task_id": "call_002",
138///       "title": "Checked user permissions",
139///       "status": "pending"
140///     },
141///     {
142///       "type": "task_card",
143///       "task_id": "call_003",
144///       "title": "Generated comprehensive user report",
145///       "status": "complete",
146///       "output": {
147///         "type": "rich_text",
148///         "block_id": "crsk",
149///         "elements": [
150///           {
151///             "type": "rich_text_section",
152///             "elements": [
153///               {
154///                 "type": "text",
155///                 "text": "15 data points compiled"
156///               }
157///             ]
158///           }
159///         ]
160///       }
161///     }
162///   ]
163/// });
164///
165/// let json = serde_json::to_value(plan).unwrap();
166///
167/// assert_eq!(json, expected);
168/// #     Ok(())
169/// # }
170/// # fn main() {
171/// #     try_main().unwrap()
172/// # }
173///```
174#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
175#[serde(tag = "type", rename = "plan")]
176pub struct Plan {
177    #[builder(validate("required"))]
178    pub(crate) title: Option<String>,
179
180    #[serde(skip_serializing_if = "Option::is_none")]
181    #[builder(validate("text::max_255"))]
182    pub(crate) block_id: Option<String>,
183
184    #[serde(skip_serializing_if = "Option::is_none")]
185    #[builder(push_item = "task")]
186    pub(crate) tasks: Option<Vec<TaskCard>>,
187}
188
189#[cfg(test)]
190mod tests {
191    use super::super::test_helpers::*;
192    use super::*;
193    use crate::errors::*;
194
195    #[test]
196    fn it_implements_builder() {
197        let expected = Plan {
198            title: Some("Thinking completed".into()),
199            block_id: Some("block_0".into()),
200            tasks: Some(vec![task_card()]),
201        };
202
203        let val = Plan::builder()
204            .set_title(Some("Thinking completed"))
205            .set_block_id(Some("block_0"))
206            .set_tasks(Some(vec![task_card()]))
207            .build()
208            .unwrap();
209
210        assert_eq!(val, expected);
211
212        let val = Plan::builder()
213            .title("Thinking completed")
214            .block_id("block_0")
215            .tasks(vec![task_card()])
216            .build()
217            .unwrap();
218
219        assert_eq!(val, expected);
220    }
221
222    #[test]
223    fn it_implements_push_item_method() {
224        let expected = Plan {
225            title: Some("Thinking completed".into()),
226            block_id: None,
227            tasks: Some(vec![task_card()]),
228        };
229
230        let val = Plan::builder()
231            .title("Thinking completed")
232            .task(task_card())
233            .build()
234            .unwrap();
235
236        assert_eq!(val, expected);
237    }
238
239    #[test]
240    fn it_requires_title_field() {
241        let err = Plan::builder().build().unwrap_err();
242        assert_eq!(err.object(), "Plan");
243
244        let errors = err.field("title");
245        assert!(errors.includes(ValidationErrorKind::Required));
246    }
247
248    #[test]
249    fn it_requires_block_id_less_than_255_characters_long() {
250        let err = Plan::builder()
251            .title("Thinking completed")
252            .block_id("b".repeat(256))
253            .build()
254            .unwrap_err();
255        assert_eq!(err.object(), "Plan");
256
257        let errors = err.field("block_id");
258        assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
259    }
260}