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