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