slack_messaging/blocks/
task_card.rs1use crate::blocks::RichText;
2use crate::blocks::elements::UrlSource;
3use crate::validators::*;
4
5use serde::Serialize;
6use slack_messaging_derive::Builder;
7
8#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
110#[serde(tag = "type", rename = "task_card")]
111pub struct TaskCard {
112 #[builder(validate("required"))]
113 pub(crate) task_id: Option<String>,
114
115 #[builder(validate("required"))]
116 pub(crate) title: Option<String>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
119 #[builder(validate("rich_text::single_element"))]
120 pub(crate) details: Option<RichText>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
123 #[builder(validate("rich_text::single_element"))]
124 pub(crate) output: Option<RichText>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
127 #[builder(push_item = "source")]
128 pub(crate) sources: Option<Vec<UrlSource>>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub(crate) status: Option<TaskStatus>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
134 #[builder(validate("text::max_255"))]
135 pub(crate) block_id: Option<String>,
136}
137
138#[derive(Debug, Copy, Clone, Serialize, PartialEq)]
140#[serde(rename_all = "snake_case")]
141pub enum TaskStatus {
142 Pending,
143 InProgress,
144 Complete,
145 Error,
146}
147
148#[cfg(test)]
149mod tests {
150 use super::super::rich_text::{
151 RichText, test_helpers::section, types::RichTextElementType, types::test_helpers::el_text,
152 };
153 use super::*;
154 use crate::blocks::rich_text::RichTextSubElement;
155 use crate::errors::*;
156
157 #[test]
158 fn it_implements_builder() {
159 let output = rich_text(vec![el_text("output of task card")]);
160 let details = rich_text(vec![el_text("details for task card")]);
161 let sources = vec![
162 url_source("https://weather.com/", "weather.com"),
163 url_source("https://www.accuweather.com/", "accuweather.com"),
164 ];
165
166 let expected = TaskCard {
167 task_id: Some("task_1".into()),
168 title: Some("Fetching weather data".into()),
169 status: Some(TaskStatus::Pending),
170 output: Some(output.clone()),
171 details: Some(details.clone()),
172 sources: Some(sources.clone()),
173 block_id: Some("task_card_0".into()),
174 };
175
176 let val = TaskCard::builder()
177 .set_task_id(Some("task_1"))
178 .set_title(Some("Fetching weather data"))
179 .set_status(Some(TaskStatus::Pending))
180 .set_output(Some(output.clone()))
181 .set_details(Some(details.clone()))
182 .set_sources(Some(sources.clone()))
183 .set_block_id(Some("task_card_0"))
184 .build()
185 .unwrap();
186
187 assert_eq!(val, expected);
188
189 let val = TaskCard::builder()
190 .task_id("task_1")
191 .title("Fetching weather data")
192 .status(TaskStatus::Pending)
193 .output(output.clone())
194 .details(details.clone())
195 .sources(sources.clone())
196 .block_id("task_card_0")
197 .build()
198 .unwrap();
199
200 assert_eq!(val, expected);
201 }
202
203 #[test]
204 fn it_implements_push_item_method() {
205 let expected = TaskCard {
206 task_id: Some("task_1".into()),
207 title: Some("Fetching weather data".into()),
208 status: None,
209 output: None,
210 details: None,
211 sources: Some(vec![
212 url_source("https://weather.com/", "weather.com"),
213 url_source("https://www.accuweather.com/", "accuweather.com"),
214 ]),
215 block_id: None,
216 };
217
218 let val = TaskCard::builder()
219 .task_id("task_1")
220 .title("Fetching weather data")
221 .source(url_source("https://weather.com/", "weather.com"))
222 .source(url_source(
223 "https://www.accuweather.com/",
224 "accuweather.com",
225 ))
226 .build()
227 .unwrap();
228
229 assert_eq!(val, expected);
230 }
231
232 #[test]
233 fn it_requires_task_id_field() {
234 let err = TaskCard::builder().title("foo").build().unwrap_err();
235 assert_eq!(err.object(), "TaskCard");
236
237 let errors = err.field("task_id");
238 assert!(errors.includes(ValidationErrorKind::Required));
239 }
240
241 #[test]
242 fn it_requires_title_field() {
243 let err = TaskCard::builder().task_id("task_1").build().unwrap_err();
244 assert_eq!(err.object(), "TaskCard");
245
246 let errors = err.field("title");
247 assert!(errors.includes(ValidationErrorKind::Required));
248 }
249
250 #[test]
251 fn it_requires_details_and_output_fields_to_have_exactly_one_element() {
252 let err = TaskCard::builder()
253 .task_id("task_1")
254 .title("foo")
255 .details(rich_text(vec![]))
256 .build()
257 .unwrap_err();
258 assert_eq!(err.object(), "TaskCard");
259
260 let errors = err.field("details");
261 assert!(errors.includes(ValidationErrorKind::RichTextSingleElement));
262
263 let err = TaskCard::builder()
264 .task_id("task_1")
265 .title("foo")
266 .output(rich_text(vec![]))
267 .build()
268 .unwrap_err();
269 assert_eq!(err.object(), "TaskCard");
270
271 let errors = err.field("output");
272 assert!(errors.includes(ValidationErrorKind::RichTextSingleElement));
273 }
274
275 #[test]
276 fn it_requires_block_id_less_than_255_characters_long() {
277 let err = TaskCard::builder()
278 .task_id("task_1")
279 .title("foo")
280 .block_id("a".repeat(256))
281 .build()
282 .unwrap_err();
283 assert_eq!(err.object(), "TaskCard");
284
285 let errors = err.field("block_id");
286 assert!(errors.includes(ValidationErrorKind::MaxTextLength(255)));
287 }
288
289 fn rich_text(texts: Vec<RichTextElementType>) -> RichText {
290 RichText {
291 block_id: None,
292 elements: Some(
293 texts
294 .into_iter()
295 .map(|text| section(vec![text]))
296 .map(RichTextSubElement::from)
297 .collect(),
298 ),
299 }
300 }
301
302 fn url_source(url: &str, text: &str) -> UrlSource {
303 UrlSource {
304 url: Some(url.into()),
305 text: Some(text.into()),
306 }
307 }
308}