slack_rust/views/
open.rs

1use crate::error::Error;
2use crate::http_client::{get_slack_url, ResponseMetadata, SlackWebAPIClient};
3use crate::views::view::View;
4use serde::{Deserialize, Serialize};
5use serde_with::skip_serializing_none;
6
7#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
8pub struct OpenRequest {
9    pub trigger_id: String,
10    pub view: View,
11}
12
13#[skip_serializing_none]
14#[derive(Deserialize, Serialize, Debug, Default, PartialEq)]
15pub struct OpenResponse {
16    pub ok: bool,
17    pub error: Option<String>,
18    pub response_metadata: Option<ResponseMetadata>,
19    pub view: Option<View>,
20}
21
22pub async fn open<T>(
23    client: &T,
24    param: &OpenRequest,
25    bot_token: &str,
26) -> Result<OpenResponse, Error>
27where
28    T: SlackWebAPIClient,
29{
30    let url = get_slack_url("views.open");
31    let json = serde_json::to_string(&param)?;
32
33    client
34        .post_json(&url, &json, bot_token)
35        .await
36        .and_then(|result| {
37            serde_json::from_str::<OpenResponse>(&result).map_err(Error::SerdeJsonError)
38        })
39}
40
41#[cfg(test)]
42mod test {
43    use super::*;
44    use crate::block::block_actions::ActionBlock;
45    use crate::block::block_elements::{
46        BlockElement, ButtonElement, MultiSelectBlockElement, PlainTextInputBlockElement,
47    };
48    use crate::block::block_input::InputBlock;
49    use crate::block::block_object::{OptionBlockObject, TextBlockObject, TextBlockType};
50    use crate::block::blocks::Block;
51
52    use crate::http_client::MockSlackWebAPIClient;
53    use crate::views::view::ViewType;
54
55    #[test]
56    fn convert_request() {
57        let request = OpenRequest {
58            trigger_id: "12345.98765.abcd2358fdea".to_string(),
59            view: View {
60                type_filed: Some(ViewType::Modal),
61                title: Some(TextBlockObject {
62                    type_filed: TextBlockType::PlainText,
63                    text: "Slack Rust Example Modal".to_string(),
64                    ..Default::default()
65                }),
66                submit: Some(TextBlockObject {
67                    type_filed: TextBlockType::PlainText,
68                    text: "Submit".to_string(),
69                    ..Default::default()
70                }),
71                blocks: Some(vec![
72                    Block::InputBlock(InputBlock {
73                        label: TextBlockObject {
74                            type_filed: TextBlockType::PlainText,
75                            text: "Title".to_string(),
76                            ..Default::default()
77                        },
78                        element: BlockElement::PlainTextInputBlockElement(
79                            PlainTextInputBlockElement {
80                                action_id: "title".to_string(),
81                                placeholder: Some(TextBlockObject {
82                                    type_filed: TextBlockType::PlainText,
83                                    text: "What do you want to ask of the world?".to_string(),
84                                    ..Default::default()
85                                }),
86                                ..Default::default()
87                            },
88                        ),
89                        ..Default::default()
90                    }),
91                    Block::InputBlock(InputBlock {
92                        label: TextBlockObject {
93                            type_filed: TextBlockType::PlainText,
94                            text: "Channel(s)".to_string(),
95                            ..Default::default()
96                        },
97                        element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
98                            action_id: "title".to_string(),
99                            placeholder: TextBlockObject {
100                                type_filed: TextBlockType::PlainText,
101                                text: "Where should the poll be sent?".to_string(),
102                                ..Default::default()
103                            },
104                            options: vec![OptionBlockObject {
105                                text: TextBlockObject {
106                                    type_filed: TextBlockType::PlainText,
107                                    text: "*this is plain_text text*".to_string(),
108                                    ..Default::default()
109                                },
110                                value: Some("value-0".to_string()),
111                                ..Default::default()
112                            }],
113                            ..Default::default()
114                        }),
115                        ..Default::default()
116                    }),
117                    Block::ActionBlock(ActionBlock {
118                        elements: vec![BlockElement::ButtonElement(ButtonElement {
119                            action_id: "add_option".to_string(),
120                            text: TextBlockObject {
121                                type_filed: TextBlockType::PlainText,
122                                text: "Add another option".to_string(),
123                                ..Default::default()
124                            },
125                            ..Default::default()
126                        })],
127                        ..Default::default()
128                    }),
129                ]),
130                ..Default::default()
131            },
132        };
133        let json = r##"{
134  "trigger_id": "12345.98765.abcd2358fdea",
135  "view": {
136    "type": "modal",
137    "blocks": [
138      {
139        "type": "input",
140        "label": {
141          "type": "plain_text",
142          "text": "Title"
143        },
144        "element": {
145          "type": "plain_text_input",
146          "action_id": "title",
147          "placeholder": {
148            "type": "plain_text",
149            "text": "What do you want to ask of the world?"
150          }
151        }
152      },
153      {
154        "type": "input",
155        "label": {
156          "type": "plain_text",
157          "text": "Channel(s)"
158        },
159        "element": {
160          "type": "multi_static_select",
161          "placeholder": {
162            "type": "plain_text",
163            "text": "Where should the poll be sent?"
164          },
165          "action_id": "title",
166          "options": [
167            {
168              "text": {
169                "type": "plain_text",
170                "text": "*this is plain_text text*"
171              },
172              "value": "value-0"
173            }
174          ]
175        }
176      },
177      {
178        "type": "actions",
179        "elements": [
180          {
181            "type": "button",
182            "text": {
183              "type": "plain_text",
184              "text": "Add another option"
185            },
186            "action_id": "add_option"
187          }
188        ]
189      }
190    ],
191    "title": {
192      "type": "plain_text",
193      "text": "Slack Rust Example Modal"
194    },
195    "submit": {
196      "type": "plain_text",
197      "text": "Submit"
198    }
199  }
200}"##;
201
202        let j = serde_json::to_string_pretty(&request).unwrap();
203        assert_eq!(json, j);
204
205        let s = serde_json::from_str::<OpenRequest>(json).unwrap();
206        assert_eq!(request, s);
207    }
208
209    #[test]
210    fn convert_response() {
211        let response = OpenResponse {
212            ok: true,
213            view: Some(View {
214                type_filed: Some(ViewType::Modal),
215                title: Some(TextBlockObject {
216                    type_filed: TextBlockType::PlainText,
217                    text: "Slack Rust Example Modal".to_string(),
218                    ..Default::default()
219                }),
220                submit: Some(TextBlockObject {
221                    type_filed: TextBlockType::PlainText,
222                    text: "Submit".to_string(),
223                    ..Default::default()
224                }),
225                blocks: Some(vec![
226                    Block::InputBlock(InputBlock {
227                        label: TextBlockObject {
228                            type_filed: TextBlockType::PlainText,
229                            text: "Title".to_string(),
230                            ..Default::default()
231                        },
232                        element: BlockElement::PlainTextInputBlockElement(
233                            PlainTextInputBlockElement {
234                                action_id: "title".to_string(),
235                                placeholder: Some(TextBlockObject {
236                                    type_filed: TextBlockType::PlainText,
237                                    text: "What do you want to ask of the world?".to_string(),
238                                    ..Default::default()
239                                }),
240                                ..Default::default()
241                            },
242                        ),
243                        ..Default::default()
244                    }),
245                    Block::InputBlock(InputBlock {
246                        label: TextBlockObject {
247                            type_filed: TextBlockType::PlainText,
248                            text: "Channel(s)".to_string(),
249                            ..Default::default()
250                        },
251                        element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
252                            action_id: "title".to_string(),
253                            placeholder: TextBlockObject {
254                                type_filed: TextBlockType::PlainText,
255                                text: "Where should the poll be sent?".to_string(),
256                                ..Default::default()
257                            },
258                            options: vec![OptionBlockObject {
259                                text: TextBlockObject {
260                                    type_filed: TextBlockType::PlainText,
261                                    text: "*this is plain_text text*".to_string(),
262                                    ..Default::default()
263                                },
264                                value: Some("value-0".to_string()),
265                                ..Default::default()
266                            }],
267                            ..Default::default()
268                        }),
269                        ..Default::default()
270                    }),
271                    Block::ActionBlock(ActionBlock {
272                        elements: vec![BlockElement::ButtonElement(ButtonElement {
273                            action_id: "add_option".to_string(),
274                            text: TextBlockObject {
275                                type_filed: TextBlockType::PlainText,
276                                text: "Add another option".to_string(),
277                                ..Default::default()
278                            },
279                            ..Default::default()
280                        })],
281                        ..Default::default()
282                    }),
283                ]),
284                ..Default::default()
285            }),
286            ..Default::default()
287        };
288        let json = r##"{
289  "ok": true,
290  "view": {
291    "type": "modal",
292    "blocks": [
293      {
294        "type": "input",
295        "label": {
296          "type": "plain_text",
297          "text": "Title"
298        },
299        "element": {
300          "type": "plain_text_input",
301          "action_id": "title",
302          "placeholder": {
303            "type": "plain_text",
304            "text": "What do you want to ask of the world?"
305          }
306        }
307      },
308      {
309        "type": "input",
310        "label": {
311          "type": "plain_text",
312          "text": "Channel(s)"
313        },
314        "element": {
315          "type": "multi_static_select",
316          "placeholder": {
317            "type": "plain_text",
318            "text": "Where should the poll be sent?"
319          },
320          "action_id": "title",
321          "options": [
322            {
323              "text": {
324                "type": "plain_text",
325                "text": "*this is plain_text text*"
326              },
327              "value": "value-0"
328            }
329          ]
330        }
331      },
332      {
333        "type": "actions",
334        "elements": [
335          {
336            "type": "button",
337            "text": {
338              "type": "plain_text",
339              "text": "Add another option"
340            },
341            "action_id": "add_option"
342          }
343        ]
344      }
345    ],
346    "title": {
347      "type": "plain_text",
348      "text": "Slack Rust Example Modal"
349    },
350    "submit": {
351      "type": "plain_text",
352      "text": "Submit"
353    }
354  }
355}"##;
356
357        let j = serde_json::to_string_pretty(&response).unwrap();
358        assert_eq!(json, j);
359
360        let s = serde_json::from_str::<OpenResponse>(json).unwrap();
361        assert_eq!(response, s);
362    }
363
364    #[async_std::test]
365    async fn test_open() {
366        let param = OpenRequest {
367            trigger_id: "12345.98765.abcd2358fdea".to_string(),
368            view: View {
369                type_filed: Some(ViewType::Modal),
370                title: Some(TextBlockObject {
371                    type_filed: TextBlockType::PlainText,
372                    text: "Slack Rust Example Modal".to_string(),
373                    ..Default::default()
374                }),
375                submit: Some(TextBlockObject {
376                    type_filed: TextBlockType::PlainText,
377                    text: "Submit".to_string(),
378                    ..Default::default()
379                }),
380                blocks: Some(vec![
381                    Block::InputBlock(InputBlock {
382                        label: TextBlockObject {
383                            type_filed: TextBlockType::PlainText,
384                            text: "Title".to_string(),
385                            ..Default::default()
386                        },
387                        element: BlockElement::PlainTextInputBlockElement(
388                            PlainTextInputBlockElement {
389                                action_id: "title".to_string(),
390                                placeholder: Some(TextBlockObject {
391                                    type_filed: TextBlockType::PlainText,
392                                    text: "What do you want to ask of the world?".to_string(),
393                                    ..Default::default()
394                                }),
395                                ..Default::default()
396                            },
397                        ),
398                        ..Default::default()
399                    }),
400                    Block::InputBlock(InputBlock {
401                        label: TextBlockObject {
402                            type_filed: TextBlockType::PlainText,
403                            text: "Channel(s)".to_string(),
404                            ..Default::default()
405                        },
406                        element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
407                            action_id: "title".to_string(),
408                            placeholder: TextBlockObject {
409                                type_filed: TextBlockType::PlainText,
410                                text: "Where should the poll be sent?".to_string(),
411                                ..Default::default()
412                            },
413                            options: vec![OptionBlockObject {
414                                text: TextBlockObject {
415                                    type_filed: TextBlockType::PlainText,
416                                    text: "*this is plain_text text*".to_string(),
417                                    ..Default::default()
418                                },
419                                value: Some("value-0".to_string()),
420                                ..Default::default()
421                            }],
422                            ..Default::default()
423                        }),
424                        ..Default::default()
425                    }),
426                    Block::ActionBlock(ActionBlock {
427                        elements: vec![BlockElement::ButtonElement(ButtonElement {
428                            action_id: "add_option".to_string(),
429                            text: TextBlockObject {
430                                type_filed: TextBlockType::PlainText,
431                                text: "Add another option".to_string(),
432                                ..Default::default()
433                            },
434                            ..Default::default()
435                        })],
436                        ..Default::default()
437                    }),
438                ]),
439                ..Default::default()
440            },
441        };
442        let mut mock = MockSlackWebAPIClient::new();
443        mock.expect_post_json().returning(|_, _, _| {
444            Ok(r##"{
445  "ok": true,
446  "view": {
447    "type": "modal",
448    "blocks": [
449      {
450        "type": "input",
451        "label": {
452          "type": "plain_text",
453          "text": "Title"
454        },
455        "element": {
456          "type": "plain_text_input",
457          "action_id": "title",
458          "placeholder": {
459            "type": "plain_text",
460            "text": "What do you want to ask of the world?"
461          }
462        }
463      },
464      {
465        "type": "input",
466        "label": {
467          "type": "plain_text",
468          "text": "Channel(s)"
469        },
470        "element": {
471          "type": "multi_static_select",
472          "placeholder": {
473            "type": "plain_text",
474            "text": "Where should the poll be sent?"
475          },
476          "action_id": "title",
477          "options": [
478            {
479              "text": {
480                "type": "plain_text",
481                "text": "*this is plain_text text*"
482              },
483              "value": "value-0"
484            }
485          ]
486        }
487      },
488      {
489        "type": "actions",
490        "elements": [
491          {
492            "type": "button",
493            "text": {
494              "type": "plain_text",
495              "text": "Add another option"
496            },
497            "action_id": "add_option"
498          }
499        ]
500      }
501    ],
502    "title": {
503      "type": "plain_text",
504      "text": "Slack Rust Example Modal"
505    },
506    "submit": {
507      "type": "plain_text",
508      "text": "Submit"
509    }
510  }
511}"##
512            .to_string())
513        });
514
515        let response = open(&mock, &param, &"test_token".to_string())
516            .await
517            .unwrap();
518        let expect = OpenResponse {
519            ok: true,
520            view: Some(View {
521                type_filed: Some(ViewType::Modal),
522                title: Some(TextBlockObject {
523                    type_filed: TextBlockType::PlainText,
524                    text: "Slack Rust Example Modal".to_string(),
525                    ..Default::default()
526                }),
527                submit: Some(TextBlockObject {
528                    type_filed: TextBlockType::PlainText,
529                    text: "Submit".to_string(),
530                    ..Default::default()
531                }),
532                blocks: Some(vec![
533                    Block::InputBlock(InputBlock {
534                        label: TextBlockObject {
535                            type_filed: TextBlockType::PlainText,
536                            text: "Title".to_string(),
537                            ..Default::default()
538                        },
539                        element: BlockElement::PlainTextInputBlockElement(
540                            PlainTextInputBlockElement {
541                                action_id: "title".to_string(),
542                                placeholder: Some(TextBlockObject {
543                                    type_filed: TextBlockType::PlainText,
544                                    text: "What do you want to ask of the world?".to_string(),
545                                    ..Default::default()
546                                }),
547                                ..Default::default()
548                            },
549                        ),
550                        ..Default::default()
551                    }),
552                    Block::InputBlock(InputBlock {
553                        label: TextBlockObject {
554                            type_filed: TextBlockType::PlainText,
555                            text: "Channel(s)".to_string(),
556                            ..Default::default()
557                        },
558                        element: BlockElement::MultiSelectBlockElement(MultiSelectBlockElement {
559                            action_id: "title".to_string(),
560                            placeholder: TextBlockObject {
561                                type_filed: TextBlockType::PlainText,
562                                text: "Where should the poll be sent?".to_string(),
563                                ..Default::default()
564                            },
565                            options: vec![OptionBlockObject {
566                                text: TextBlockObject {
567                                    type_filed: TextBlockType::PlainText,
568                                    text: "*this is plain_text text*".to_string(),
569                                    ..Default::default()
570                                },
571                                value: Some("value-0".to_string()),
572                                ..Default::default()
573                            }],
574                            ..Default::default()
575                        }),
576                        ..Default::default()
577                    }),
578                    Block::ActionBlock(ActionBlock {
579                        elements: vec![BlockElement::ButtonElement(ButtonElement {
580                            action_id: "add_option".to_string(),
581                            text: TextBlockObject {
582                                type_filed: TextBlockType::PlainText,
583                                text: "Add another option".to_string(),
584                                ..Default::default()
585                            },
586                            ..Default::default()
587                        })],
588                        ..Default::default()
589                    }),
590                ]),
591                ..Default::default()
592            }),
593            ..Default::default()
594        };
595
596        assert_eq!(expect, response);
597    }
598}