Skip to main content

slack_morphism/models/blocks/
kit.rs

1use rsb_derive::Builder;
2use rvstruct::ValueStruct;
3use serde::{Deserialize, Serialize};
4use serde_with::skip_serializing_none;
5use url::Url;
6
7use super::workflow::SlackBlockWorkflowButtonElement;
8use crate::*;
9
10#[skip_serializing_none]
11#[derive(Debug, PartialEq, Clone, Eq, Hash, Serialize, Deserialize, ValueStruct)]
12pub struct SlackBlockId(pub String);
13
14#[derive(Debug, PartialEq, Clone, Eq, Hash, Serialize, Deserialize, ValueStruct)]
15pub struct SlackTaskId(pub String);
16
17#[derive(Debug, PartialEq, Clone, Eq, Hash, Serialize, Deserialize, ValueStruct)]
18pub struct SlackAccessibilityLabel(pub String);
19
20/// Header block heading level: 1–4 = H1–H4.
21#[derive(
22    Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize, ValueStruct,
23)]
24pub struct SlackHeaderLevel(pub u8);
25
26/// Blocks are internally tagged by `type`. A block whose `type` the crate does
27/// not model — or whose body does not match its known `type` — lands in
28/// [`SlackBlock::Unknown`] with its raw JSON instead of failing the whole
29/// `Vec<SlackBlock>`. `Unknown` must stay the last variant.
30#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
31#[serde(tag = "type")]
32pub enum SlackBlock {
33    #[serde(rename = "section")]
34    Section(SlackSectionBlock),
35    #[serde(rename = "header")]
36    Header(SlackHeaderBlock),
37    #[serde(rename = "divider")]
38    Divider(SlackDividerBlock),
39    #[serde(rename = "image")]
40    Image(SlackImageBlock),
41    #[serde(rename = "actions")]
42    Actions(SlackActionsBlock),
43    #[serde(rename = "context")]
44    Context(SlackContextBlock),
45    #[serde(rename = "input")]
46    Input(SlackInputBlock),
47    #[serde(rename = "file")]
48    File(SlackFileBlock),
49    #[serde(rename = "video")]
50    Video(SlackVideoBlock),
51    #[serde(rename = "markdown")]
52    Markdown(SlackMarkdownBlock),
53    #[serde(rename = "rich_text")]
54    RichText(SlackRichTextBlock),
55    #[serde(rename = "table")]
56    Table(SlackTableBlock),
57    #[serde(rename = "task_card")]
58    TaskCard(SlackTaskCardBlock),
59    #[serde(rename = "alert")]
60    Alert(SlackAlertBlock),
61    #[serde(rename = "card")]
62    Card(SlackCardBlock),
63    #[serde(rename = "carousel")]
64    Carousel(SlackCarouselBlock),
65    #[serde(rename = "context_actions")]
66    ContextActions(SlackContextActionsBlock),
67    #[serde(rename = "container")]
68    Container(SlackContainerBlock),
69    #[serde(rename = "plan")]
70    Plan(SlackPlanBlock),
71    #[serde(rename = "data_table")]
72    DataTable(SlackDataTableBlock),
73    #[serde(rename = "data_visualization")]
74    DataVisualization(SlackDataVisualizationBlock),
75    #[serde(rename = "share_shortcut")]
76    ShareShortcut(serde_json::Value),
77    #[serde(rename = "event")]
78    Event(serde_json::Value),
79    #[serde(untagged)]
80    Unknown(serde_json::Value),
81}
82
83#[skip_serializing_none]
84#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
85pub struct SlackSectionBlock {
86    pub block_id: Option<SlackBlockId>,
87    pub text: Option<SlackBlockText>,
88    pub fields: Option<Vec<SlackBlockText>>,
89    pub accessory: Option<SlackSectionBlockElement>,
90    pub expand: Option<bool>,
91}
92
93impl From<SlackSectionBlock> for SlackBlock {
94    fn from(block: SlackSectionBlock) -> Self {
95        SlackBlock::Section(block)
96    }
97}
98
99#[skip_serializing_none]
100#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
101pub struct SlackHeaderBlock {
102    pub block_id: Option<SlackBlockId>,
103    pub text: SlackBlockPlainTextOnly,
104    /// 1–4 = H1–H4; Slack renders the default size when absent.
105    /// <https://docs.slack.dev/reference/block-kit/blocks/header-block>
106    pub level: Option<SlackHeaderLevel>,
107}
108
109impl From<SlackHeaderBlock> for SlackBlock {
110    fn from(block: SlackHeaderBlock) -> Self {
111        SlackBlock::Header(block)
112    }
113}
114
115#[skip_serializing_none]
116#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
117pub struct SlackDividerBlock {
118    pub block_id: Option<SlackBlockId>,
119}
120
121impl From<SlackDividerBlock> for SlackBlock {
122    fn from(block: SlackDividerBlock) -> Self {
123        SlackBlock::Divider(block)
124    }
125}
126
127#[skip_serializing_none]
128#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
129pub struct SlackImageBlock {
130    pub block_id: Option<SlackBlockId>,
131    #[serde(flatten)]
132    pub image_url_or_file: SlackImageUrlOrFile,
133    pub alt_text: String,
134    pub title: Option<SlackBlockPlainTextOnly>,
135}
136
137impl From<SlackImageBlock> for SlackBlock {
138    fn from(block: SlackImageBlock) -> Self {
139        SlackBlock::Image(block)
140    }
141}
142
143#[skip_serializing_none]
144#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
145pub struct SlackActionsBlock {
146    pub block_id: Option<SlackBlockId>,
147    pub elements: Vec<SlackActionBlockElement>,
148}
149
150impl From<SlackActionsBlock> for SlackBlock {
151    fn from(block: SlackActionsBlock) -> Self {
152        SlackBlock::Actions(block)
153    }
154}
155
156#[skip_serializing_none]
157#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
158pub struct SlackContextBlock {
159    pub block_id: Option<SlackBlockId>,
160    pub elements: Vec<SlackContextBlockElement>,
161}
162
163impl From<SlackContextBlock> for SlackBlock {
164    fn from(block: SlackContextBlock) -> Self {
165        SlackBlock::Context(block)
166    }
167}
168
169#[skip_serializing_none]
170#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
171pub struct SlackInputBlock {
172    pub block_id: Option<SlackBlockId>,
173    pub label: SlackBlockPlainTextOnly,
174    pub element: SlackInputBlockElement,
175    pub hint: Option<SlackBlockPlainTextOnly>,
176    pub optional: Option<bool>,
177    pub dispatch_action: Option<bool>,
178}
179
180impl From<SlackInputBlock> for SlackBlock {
181    fn from(block: SlackInputBlock) -> Self {
182        SlackBlock::Input(block)
183    }
184}
185
186const SLACK_FILE_BLOCK_SOURCE_DEFAULT: &str = "remote";
187
188#[skip_serializing_none]
189#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
190pub struct SlackFileBlock {
191    pub block_id: Option<SlackBlockId>,
192    pub external_id: String,
193    #[default = "SLACK_FILE_BLOCK_SOURCE_DEFAULT.into()"]
194    pub source: String,
195}
196
197impl From<SlackFileBlock> for SlackBlock {
198    fn from(block: SlackFileBlock) -> Self {
199        SlackBlock::File(block)
200    }
201}
202
203#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
204#[serde(tag = "type")]
205pub enum SlackSectionBlockElement {
206    #[serde(rename = "image")]
207    Image(SlackBlockImageElement),
208    #[serde(rename = "button")]
209    Button(SlackBlockButtonElement),
210    #[serde(rename = "static_select")]
211    StaticSelect(SlackBlockStaticSelectElement),
212    #[serde(rename = "multi_static_select")]
213    MultiStaticSelect(SlackBlockMultiStaticSelectElement),
214    #[serde(rename = "external_select")]
215    ExternalSelect(SlackBlockExternalSelectElement),
216    #[serde(rename = "multi_external_select")]
217    MultiExternalSelect(SlackBlockMultiExternalSelectElement),
218    #[serde(rename = "users_select")]
219    UsersSelect(SlackBlockUsersSelectElement),
220    #[serde(rename = "multi_users_select")]
221    MultiUsersSelect(SlackBlockMultiUsersSelectElement),
222    #[serde(rename = "conversations_select")]
223    ConversationsSelect(SlackBlockConversationsSelectElement),
224    #[serde(rename = "multi_conversations_select")]
225    MultiConversationsSelect(SlackBlockMultiConversationsSelectElement),
226    #[serde(rename = "channels_select")]
227    ChannelsSelect(SlackBlockChannelsSelectElement),
228    #[serde(rename = "multi_channels_select")]
229    MultiChannelsSelect(SlackBlockMultiChannelsSelectElement),
230    #[serde(rename = "overflow")]
231    Overflow(SlackBlockOverflowElement),
232    #[serde(rename = "datepicker")]
233    DatePicker(SlackBlockDatePickerElement),
234    #[serde(rename = "timepicker")]
235    TimePicker(SlackBlockTimePickerElement),
236    #[serde(rename = "plain_text_input")]
237    PlainTextInput(SlackBlockPlainTextInputElement),
238    #[serde(rename = "number_input")]
239    NumberInput(SlackBlockNumberInputElement),
240    #[serde(rename = "url_text_input")]
241    UrlInput(SlackBlockUrlInputElement),
242    #[serde(rename = "radio_buttons")]
243    RadioButtons(SlackBlockRadioButtonsElement),
244    #[serde(rename = "checkboxes")]
245    Checkboxes(SlackBlockCheckboxesElement),
246    #[serde(rename = "workflow_button")]
247    WorkflowButton(SlackBlockWorkflowButtonElement),
248}
249
250#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
251#[serde(tag = "type")]
252pub enum SlackActionBlockElement {
253    #[serde(rename = "button")]
254    Button(SlackBlockButtonElement),
255    #[serde(rename = "overflow")]
256    Overflow(SlackBlockOverflowElement),
257    #[serde(rename = "datepicker")]
258    DatePicker(SlackBlockDatePickerElement),
259    #[serde(rename = "timepicker")]
260    TimePicker(SlackBlockTimePickerElement),
261    #[serde(rename = "datetimepicker")]
262    DateTimePicker(SlackBlockDateTimePickerElement),
263    #[serde(rename = "plain_text_input")]
264    PlainTextInput(SlackBlockPlainTextInputElement),
265    #[serde(rename = "number_input")]
266    NumberInput(SlackBlockNumberInputElement),
267    #[serde(rename = "url_text_input")]
268    UrlInput(SlackBlockUrlInputElement),
269    #[serde(rename = "radio_buttons")]
270    RadioButtons(SlackBlockRadioButtonsElement),
271    #[serde(rename = "checkboxes")]
272    Checkboxes(SlackBlockCheckboxesElement),
273    #[serde(rename = "static_select")]
274    StaticSelect(SlackBlockStaticSelectElement),
275    #[serde(rename = "external_select")]
276    ExternalSelect(SlackBlockExternalSelectElement),
277    #[serde(rename = "users_select")]
278    UsersSelect(SlackBlockUsersSelectElement),
279    #[serde(rename = "conversations_select")]
280    ConversationsSelect(SlackBlockConversationsSelectElement),
281    #[serde(rename = "channels_select")]
282    ChannelsSelect(SlackBlockChannelsSelectElement),
283    #[serde(rename = "workflow_button")]
284    WorkflowButton(SlackBlockWorkflowButtonElement),
285}
286
287#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
288#[serde(tag = "type")]
289pub enum SlackContextBlockElement {
290    #[serde(rename = "image")]
291    Image(SlackBlockImageElement),
292    #[serde(rename = "plain_text")]
293    Plain(SlackBlockPlainText),
294    #[serde(rename = "mrkdwn")]
295    MarkDown(SlackBlockMarkDownText),
296}
297
298#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
299#[serde(tag = "type")]
300pub enum SlackInputBlockElement {
301    #[serde(rename = "static_select")]
302    StaticSelect(SlackBlockStaticSelectElement),
303    #[serde(rename = "multi_static_select")]
304    MultiStaticSelect(SlackBlockMultiStaticSelectElement),
305    #[serde(rename = "external_select")]
306    ExternalSelect(SlackBlockExternalSelectElement),
307    #[serde(rename = "multi_external_select")]
308    MultiExternalSelect(SlackBlockMultiExternalSelectElement),
309    #[serde(rename = "users_select")]
310    UsersSelect(SlackBlockUsersSelectElement),
311    #[serde(rename = "multi_users_select")]
312    MultiUsersSelect(SlackBlockMultiUsersSelectElement),
313    #[serde(rename = "conversations_select")]
314    ConversationsSelect(SlackBlockConversationsSelectElement),
315    #[serde(rename = "multi_conversations_select")]
316    MultiConversationsSelect(SlackBlockMultiConversationsSelectElement),
317    #[serde(rename = "channels_select")]
318    ChannelsSelect(SlackBlockChannelsSelectElement),
319    #[serde(rename = "multi_channels_select")]
320    MultiChannelsSelect(SlackBlockMultiChannelsSelectElement),
321    #[serde(rename = "datepicker")]
322    DatePicker(SlackBlockDatePickerElement),
323    #[serde(rename = "timepicker")]
324    TimePicker(SlackBlockTimePickerElement),
325    #[serde(rename = "datetimepicker")]
326    DateTimePicker(SlackBlockDateTimePickerElement),
327    #[serde(rename = "plain_text_input")]
328    PlainTextInput(SlackBlockPlainTextInputElement),
329    #[serde(rename = "number_input")]
330    NumberInput(SlackBlockNumberInputElement),
331    #[serde(rename = "url_text_input")]
332    UrlInput(SlackBlockUrlInputElement),
333    #[serde(rename = "radio_buttons")]
334    RadioButtons(SlackBlockRadioButtonsElement),
335    #[serde(rename = "checkboxes")]
336    Checkboxes(SlackBlockCheckboxesElement),
337    #[serde(rename = "email_text_input")]
338    EmailInput(SlackBlockEmailInputElement),
339    #[serde(rename = "rich_text_input")]
340    RichTextInput(SlackBlockRichTextInputElement),
341    #[serde(rename = "file_input")]
342    FileInput(SlackBlockFileInputElement),
343}
344
345#[skip_serializing_none]
346#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
347pub struct SlackBlockImageElement {
348    #[serde(flatten)]
349    pub image_url_or_file: SlackImageUrlOrFile,
350    pub alt_text: String,
351}
352
353impl From<SlackBlockImageElement> for SlackSectionBlockElement {
354    fn from(element: SlackBlockImageElement) -> Self {
355        SlackSectionBlockElement::Image(element)
356    }
357}
358
359impl From<SlackBlockImageElement> for SlackContextBlockElement {
360    fn from(element: SlackBlockImageElement) -> Self {
361        SlackContextBlockElement::Image(element)
362    }
363}
364
365#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
366#[serde(rename_all = "snake_case")]
367pub enum SlackBlockButtonStyle {
368    Primary,
369    Danger,
370}
371
372#[skip_serializing_none]
373#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
374pub struct SlackBlockButtonElement {
375    pub action_id: SlackActionId,
376    pub text: SlackBlockPlainTextOnly,
377    pub url: Option<Url>,
378    pub value: Option<String>,
379    pub style: Option<SlackBlockButtonStyle>,
380    pub confirm: Option<SlackBlockConfirmItem>,
381    pub accessibility_label: Option<SlackAccessibilityLabel>,
382}
383
384impl From<SlackBlockButtonElement> for SlackSectionBlockElement {
385    fn from(element: SlackBlockButtonElement) -> Self {
386        SlackSectionBlockElement::Button(element)
387    }
388}
389
390impl From<SlackBlockButtonElement> for SlackActionBlockElement {
391    fn from(element: SlackBlockButtonElement) -> Self {
392        SlackActionBlockElement::Button(element)
393    }
394}
395
396#[skip_serializing_none]
397#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
398pub struct SlackBlockConfirmItem {
399    pub title: SlackBlockPlainTextOnly,
400    pub text: SlackBlockText,
401    pub confirm: SlackBlockPlainTextOnly,
402    pub deny: SlackBlockPlainTextOnly,
403    pub style: Option<String>,
404}
405
406#[skip_serializing_none]
407#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
408pub struct SlackBlockChoiceItem<T: Into<SlackBlockText>> {
409    pub text: T,
410    pub value: String,
411    pub description: Option<SlackBlockPlainTextOnly>,
412    pub url: Option<Url>,
413}
414
415#[skip_serializing_none]
416#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
417pub struct SlackBlockOptionGroup<T: Into<SlackBlockText>> {
418    pub label: SlackBlockPlainTextOnly,
419    pub options: Vec<SlackBlockChoiceItem<T>>,
420}
421
422#[skip_serializing_none]
423#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
424pub struct SlackBlockStaticSelectElement {
425    pub action_id: SlackActionId,
426    pub placeholder: Option<SlackBlockPlainTextOnly>,
427    pub options: Option<Vec<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>>,
428    pub option_groups: Option<Vec<SlackBlockOptionGroup<SlackBlockPlainTextOnly>>>,
429    pub initial_option: Option<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>,
430    pub confirm: Option<SlackBlockConfirmItem>,
431    pub focus_on_load: Option<bool>,
432}
433
434impl From<SlackBlockStaticSelectElement> for SlackSectionBlockElement {
435    fn from(element: SlackBlockStaticSelectElement) -> Self {
436        SlackSectionBlockElement::StaticSelect(element)
437    }
438}
439
440impl From<SlackBlockStaticSelectElement> for SlackInputBlockElement {
441    fn from(element: SlackBlockStaticSelectElement) -> Self {
442        SlackInputBlockElement::StaticSelect(element)
443    }
444}
445
446impl From<SlackBlockStaticSelectElement> for SlackActionBlockElement {
447    fn from(element: SlackBlockStaticSelectElement) -> Self {
448        SlackActionBlockElement::StaticSelect(element)
449    }
450}
451
452#[skip_serializing_none]
453#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
454pub struct SlackBlockMultiStaticSelectElement {
455    pub action_id: SlackActionId,
456    pub placeholder: Option<SlackBlockPlainTextOnly>,
457    pub options: Option<Vec<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>>,
458    pub option_groups: Option<Vec<SlackBlockOptionGroup<SlackBlockPlainTextOnly>>>,
459    pub initial_options: Option<Vec<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>>,
460    pub confirm: Option<SlackBlockConfirmItem>,
461    pub max_selected_items: Option<u64>,
462    pub focus_on_load: Option<bool>,
463}
464
465impl From<SlackBlockMultiStaticSelectElement> for SlackSectionBlockElement {
466    fn from(element: SlackBlockMultiStaticSelectElement) -> Self {
467        SlackSectionBlockElement::MultiStaticSelect(element)
468    }
469}
470
471impl From<SlackBlockMultiStaticSelectElement> for SlackInputBlockElement {
472    fn from(element: SlackBlockMultiStaticSelectElement) -> Self {
473        SlackInputBlockElement::MultiStaticSelect(element)
474    }
475}
476
477#[skip_serializing_none]
478#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
479pub struct SlackBlockExternalSelectElement {
480    pub action_id: SlackActionId,
481    pub placeholder: Option<SlackBlockPlainTextOnly>,
482    pub initial_option: Option<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>,
483    pub confirm: Option<SlackBlockConfirmItem>,
484    pub focus_on_load: Option<bool>,
485    pub min_query_length: Option<u64>,
486}
487
488impl From<SlackBlockExternalSelectElement> for SlackSectionBlockElement {
489    fn from(element: SlackBlockExternalSelectElement) -> Self {
490        SlackSectionBlockElement::ExternalSelect(element)
491    }
492}
493
494impl From<SlackBlockExternalSelectElement> for SlackInputBlockElement {
495    fn from(element: SlackBlockExternalSelectElement) -> Self {
496        SlackInputBlockElement::ExternalSelect(element)
497    }
498}
499
500impl From<SlackBlockExternalSelectElement> for SlackActionBlockElement {
501    fn from(element: SlackBlockExternalSelectElement) -> Self {
502        SlackActionBlockElement::ExternalSelect(element)
503    }
504}
505
506#[skip_serializing_none]
507#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
508pub struct SlackBlockMultiExternalSelectElement {
509    pub action_id: SlackActionId,
510    pub placeholder: Option<SlackBlockPlainTextOnly>,
511    pub initial_options: Option<Vec<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>>,
512    pub confirm: Option<SlackBlockConfirmItem>,
513    pub max_selected_items: Option<u64>,
514    pub focus_on_load: Option<bool>,
515    pub min_query_length: Option<u64>,
516}
517
518impl From<SlackBlockMultiExternalSelectElement> for SlackSectionBlockElement {
519    fn from(element: SlackBlockMultiExternalSelectElement) -> Self {
520        SlackSectionBlockElement::MultiExternalSelect(element)
521    }
522}
523
524impl From<SlackBlockMultiExternalSelectElement> for SlackInputBlockElement {
525    fn from(element: SlackBlockMultiExternalSelectElement) -> Self {
526        SlackInputBlockElement::MultiExternalSelect(element)
527    }
528}
529
530#[skip_serializing_none]
531#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
532pub struct SlackBlockUsersSelectElement {
533    pub action_id: SlackActionId,
534    pub placeholder: Option<SlackBlockPlainTextOnly>,
535    pub initial_user: Option<String>,
536    pub confirm: Option<SlackBlockConfirmItem>,
537    pub focus_on_load: Option<bool>,
538}
539
540impl From<SlackBlockUsersSelectElement> for SlackSectionBlockElement {
541    fn from(element: SlackBlockUsersSelectElement) -> Self {
542        SlackSectionBlockElement::UsersSelect(element)
543    }
544}
545
546impl From<SlackBlockUsersSelectElement> for SlackInputBlockElement {
547    fn from(element: SlackBlockUsersSelectElement) -> Self {
548        SlackInputBlockElement::UsersSelect(element)
549    }
550}
551
552impl From<SlackBlockUsersSelectElement> for SlackActionBlockElement {
553    fn from(element: SlackBlockUsersSelectElement) -> Self {
554        SlackActionBlockElement::UsersSelect(element)
555    }
556}
557
558#[skip_serializing_none]
559#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
560pub struct SlackBlockMultiUsersSelectElement {
561    pub action_id: SlackActionId,
562    pub placeholder: Option<SlackBlockPlainTextOnly>,
563    pub initial_users: Option<Vec<String>>,
564    pub confirm: Option<SlackBlockConfirmItem>,
565    pub max_selected_items: Option<u64>,
566    pub focus_on_load: Option<bool>,
567}
568
569impl From<SlackBlockMultiUsersSelectElement> for SlackSectionBlockElement {
570    fn from(element: SlackBlockMultiUsersSelectElement) -> Self {
571        SlackSectionBlockElement::MultiUsersSelect(element)
572    }
573}
574
575impl From<SlackBlockMultiUsersSelectElement> for SlackInputBlockElement {
576    fn from(element: SlackBlockMultiUsersSelectElement) -> Self {
577        SlackInputBlockElement::MultiUsersSelect(element)
578    }
579}
580
581#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
582pub enum SlackConversationFilterInclude {
583    #[serde(rename = "im")]
584    Im,
585    #[serde(rename = "mpim")]
586    Mpim,
587    #[serde(rename = "public")]
588    Public,
589    #[serde(rename = "private")]
590    Private,
591}
592
593#[skip_serializing_none]
594#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
595pub struct SlackBlockConversationFilter {
596    pub include: Option<Vec<SlackConversationFilterInclude>>,
597    pub exclude_external_shared_channels: Option<bool>,
598    pub exclude_bot_users: Option<bool>,
599}
600
601#[skip_serializing_none]
602#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
603pub struct SlackBlockConversationsSelectElement {
604    pub action_id: SlackActionId,
605    pub placeholder: Option<SlackBlockPlainTextOnly>,
606    pub initial_conversation: Option<SlackConversationId>,
607    pub default_to_current_conversation: Option<bool>,
608    pub confirm: Option<SlackBlockConfirmItem>,
609    pub response_url_enabled: Option<bool>,
610    pub focus_on_load: Option<bool>,
611    pub filter: Option<SlackBlockConversationFilter>,
612}
613
614impl From<SlackBlockConversationsSelectElement> for SlackSectionBlockElement {
615    fn from(element: SlackBlockConversationsSelectElement) -> Self {
616        SlackSectionBlockElement::ConversationsSelect(element)
617    }
618}
619
620impl From<SlackBlockConversationsSelectElement> for SlackInputBlockElement {
621    fn from(element: SlackBlockConversationsSelectElement) -> Self {
622        SlackInputBlockElement::ConversationsSelect(element)
623    }
624}
625
626impl From<SlackBlockConversationsSelectElement> for SlackActionBlockElement {
627    fn from(element: SlackBlockConversationsSelectElement) -> Self {
628        SlackActionBlockElement::ConversationsSelect(element)
629    }
630}
631
632#[skip_serializing_none]
633#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
634pub struct SlackBlockMultiConversationsSelectElement {
635    pub action_id: SlackActionId,
636    pub placeholder: Option<SlackBlockPlainTextOnly>,
637    pub initial_conversations: Option<Vec<SlackConversationId>>,
638    pub default_to_current_conversation: Option<bool>,
639    pub confirm: Option<SlackBlockConfirmItem>,
640    pub max_selected_items: Option<u64>,
641    pub focus_on_load: Option<bool>,
642    pub filter: Option<SlackBlockConversationFilter>,
643}
644
645impl From<SlackBlockMultiConversationsSelectElement> for SlackSectionBlockElement {
646    fn from(element: SlackBlockMultiConversationsSelectElement) -> Self {
647        SlackSectionBlockElement::MultiConversationsSelect(element)
648    }
649}
650
651impl From<SlackBlockMultiConversationsSelectElement> for SlackInputBlockElement {
652    fn from(element: SlackBlockMultiConversationsSelectElement) -> Self {
653        SlackInputBlockElement::MultiConversationsSelect(element)
654    }
655}
656
657#[skip_serializing_none]
658#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
659pub struct SlackBlockChannelsSelectElement {
660    pub action_id: SlackActionId,
661    pub placeholder: Option<SlackBlockPlainTextOnly>,
662    pub initial_channel: Option<SlackChannelId>,
663    pub confirm: Option<SlackBlockConfirmItem>,
664    pub response_url_enabled: Option<bool>,
665    pub focus_on_load: Option<bool>,
666}
667
668impl From<SlackBlockChannelsSelectElement> for SlackSectionBlockElement {
669    fn from(element: SlackBlockChannelsSelectElement) -> Self {
670        SlackSectionBlockElement::ChannelsSelect(element)
671    }
672}
673
674impl From<SlackBlockChannelsSelectElement> for SlackInputBlockElement {
675    fn from(element: SlackBlockChannelsSelectElement) -> Self {
676        SlackInputBlockElement::ChannelsSelect(element)
677    }
678}
679
680impl From<SlackBlockChannelsSelectElement> for SlackActionBlockElement {
681    fn from(element: SlackBlockChannelsSelectElement) -> Self {
682        SlackActionBlockElement::ChannelsSelect(element)
683    }
684}
685
686#[skip_serializing_none]
687#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
688pub struct SlackBlockMultiChannelsSelectElement {
689    pub action_id: SlackActionId,
690    pub placeholder: Option<SlackBlockPlainTextOnly>,
691    pub initial_channels: Option<Vec<SlackChannelId>>,
692    pub confirm: Option<SlackBlockConfirmItem>,
693    pub max_selected_items: Option<u64>,
694    pub focus_on_load: Option<bool>,
695}
696
697impl From<SlackBlockMultiChannelsSelectElement> for SlackSectionBlockElement {
698    fn from(element: SlackBlockMultiChannelsSelectElement) -> Self {
699        SlackSectionBlockElement::MultiChannelsSelect(element)
700    }
701}
702
703impl From<SlackBlockMultiChannelsSelectElement> for SlackInputBlockElement {
704    fn from(element: SlackBlockMultiChannelsSelectElement) -> Self {
705        SlackInputBlockElement::MultiChannelsSelect(element)
706    }
707}
708
709#[skip_serializing_none]
710#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
711pub struct SlackBlockOverflowElement {
712    pub action_id: SlackActionId,
713    pub options: Vec<SlackBlockChoiceItem<SlackBlockPlainTextOnly>>,
714    pub confirm: Option<SlackBlockConfirmItem>,
715}
716
717impl From<SlackBlockOverflowElement> for SlackSectionBlockElement {
718    fn from(element: SlackBlockOverflowElement) -> Self {
719        SlackSectionBlockElement::Overflow(element)
720    }
721}
722
723impl From<SlackBlockOverflowElement> for SlackActionBlockElement {
724    fn from(element: SlackBlockOverflowElement) -> Self {
725        SlackActionBlockElement::Overflow(element)
726    }
727}
728
729#[skip_serializing_none]
730#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
731pub struct SlackBlockDatePickerElement {
732    pub action_id: SlackActionId,
733    pub placeholder: Option<SlackBlockPlainTextOnly>,
734    pub initial_date: Option<String>,
735    pub confirm: Option<SlackBlockConfirmItem>,
736    pub focus_on_load: Option<bool>,
737}
738
739impl From<SlackBlockDatePickerElement> for SlackSectionBlockElement {
740    fn from(element: SlackBlockDatePickerElement) -> Self {
741        SlackSectionBlockElement::DatePicker(element)
742    }
743}
744
745impl From<SlackBlockDatePickerElement> for SlackInputBlockElement {
746    fn from(element: SlackBlockDatePickerElement) -> Self {
747        SlackInputBlockElement::DatePicker(element)
748    }
749}
750
751impl From<SlackBlockDatePickerElement> for SlackActionBlockElement {
752    fn from(element: SlackBlockDatePickerElement) -> Self {
753        SlackActionBlockElement::DatePicker(element)
754    }
755}
756
757#[skip_serializing_none]
758#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
759pub struct SlackBlockTimePickerElement {
760    pub action_id: SlackActionId,
761    pub initial_time: Option<String>,
762    pub confirm: Option<SlackBlockConfirmItem>,
763    pub focus_on_load: Option<bool>,
764    pub placeholder: Option<SlackBlockPlainTextOnly>,
765    pub timezone: Option<String>,
766}
767
768impl From<SlackBlockTimePickerElement> for SlackSectionBlockElement {
769    fn from(element: SlackBlockTimePickerElement) -> Self {
770        SlackSectionBlockElement::TimePicker(element)
771    }
772}
773
774impl From<SlackBlockTimePickerElement> for SlackInputBlockElement {
775    fn from(element: SlackBlockTimePickerElement) -> Self {
776        SlackInputBlockElement::TimePicker(element)
777    }
778}
779
780impl From<SlackBlockTimePickerElement> for SlackActionBlockElement {
781    fn from(element: SlackBlockTimePickerElement) -> Self {
782        SlackActionBlockElement::TimePicker(element)
783    }
784}
785
786#[skip_serializing_none]
787#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
788pub struct SlackBlockDateTimePickerElement {
789    pub action_id: SlackActionId,
790    pub initial_date_time: Option<SlackDateTime>,
791    pub confirm: Option<SlackBlockConfirmItem>,
792    pub focus_on_load: Option<bool>,
793}
794
795impl From<SlackBlockDateTimePickerElement> for SlackInputBlockElement {
796    fn from(element: SlackBlockDateTimePickerElement) -> Self {
797        SlackInputBlockElement::DateTimePicker(element)
798    }
799}
800
801impl From<SlackBlockDateTimePickerElement> for SlackActionBlockElement {
802    fn from(element: SlackBlockDateTimePickerElement) -> Self {
803        SlackActionBlockElement::DateTimePicker(element)
804    }
805}
806
807/**
808 * https://docs.slack.dev/reference/block-kit/composition-objects/dispatch-action-configuration-object
809 */
810#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
811#[serde(rename_all = "snake_case")]
812pub enum SlackDispatchActionTrigger {
813    OnEnterPressed,
814    OnCharacterEntered,
815}
816
817#[skip_serializing_none]
818#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
819pub struct SlackDispatchActionConfig {
820    pub trigger_actions_on: Option<Vec<SlackDispatchActionTrigger>>,
821}
822
823#[skip_serializing_none]
824#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
825pub struct SlackBlockPlainTextInputElement {
826    pub action_id: SlackActionId,
827    pub placeholder: Option<SlackBlockPlainTextOnly>,
828    pub initial_value: Option<String>,
829    pub multiline: Option<bool>,
830    pub min_length: Option<u64>,
831    pub max_length: Option<u64>,
832    pub focus_on_load: Option<bool>,
833    pub dispatch_action_config: Option<SlackDispatchActionConfig>,
834}
835
836impl From<SlackBlockPlainTextInputElement> for SlackSectionBlockElement {
837    fn from(element: SlackBlockPlainTextInputElement) -> Self {
838        SlackSectionBlockElement::PlainTextInput(element)
839    }
840}
841
842impl From<SlackBlockPlainTextInputElement> for SlackInputBlockElement {
843    fn from(element: SlackBlockPlainTextInputElement) -> Self {
844        SlackInputBlockElement::PlainTextInput(element)
845    }
846}
847
848impl From<SlackBlockPlainTextInputElement> for SlackActionBlockElement {
849    fn from(element: SlackBlockPlainTextInputElement) -> Self {
850        SlackActionBlockElement::PlainTextInput(element)
851    }
852}
853
854#[skip_serializing_none]
855#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
856pub struct SlackBlockNumberInputElement {
857    pub action_id: SlackActionId,
858    pub is_decimal_allowed: bool,
859    pub focus_on_load: Option<bool>,
860    pub placeholder: Option<SlackBlockPlainTextOnly>,
861    pub initial_value: Option<String>,
862    pub min_value: Option<String>,
863    pub max_value: Option<String>,
864}
865
866impl From<SlackBlockNumberInputElement> for SlackSectionBlockElement {
867    fn from(element: SlackBlockNumberInputElement) -> Self {
868        SlackSectionBlockElement::NumberInput(element)
869    }
870}
871
872impl From<SlackBlockNumberInputElement> for SlackInputBlockElement {
873    fn from(element: SlackBlockNumberInputElement) -> Self {
874        SlackInputBlockElement::NumberInput(element)
875    }
876}
877
878impl From<SlackBlockNumberInputElement> for SlackActionBlockElement {
879    fn from(element: SlackBlockNumberInputElement) -> Self {
880        SlackActionBlockElement::NumberInput(element)
881    }
882}
883
884#[skip_serializing_none]
885#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
886pub struct SlackBlockUrlInputElement {
887    pub action_id: SlackActionId,
888    pub placeholder: Option<SlackBlockPlainTextOnly>,
889    pub initial_value: Option<String>,
890}
891
892impl From<SlackBlockUrlInputElement> for SlackSectionBlockElement {
893    fn from(element: SlackBlockUrlInputElement) -> Self {
894        SlackSectionBlockElement::UrlInput(element)
895    }
896}
897
898impl From<SlackBlockUrlInputElement> for SlackInputBlockElement {
899    fn from(element: SlackBlockUrlInputElement) -> Self {
900        SlackInputBlockElement::UrlInput(element)
901    }
902}
903
904impl From<SlackBlockUrlInputElement> for SlackActionBlockElement {
905    fn from(element: SlackBlockUrlInputElement) -> Self {
906        SlackActionBlockElement::UrlInput(element)
907    }
908}
909
910#[skip_serializing_none]
911#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
912pub struct SlackBlockEmailInputElement {
913    pub action_id: SlackActionId,
914    pub focus_on_load: Option<bool>,
915    pub placeholder: Option<SlackBlockPlainTextOnly>,
916    pub initial_value: Option<EmailAddress>,
917}
918
919impl From<SlackBlockEmailInputElement> for SlackInputBlockElement {
920    fn from(element: SlackBlockEmailInputElement) -> Self {
921        SlackInputBlockElement::EmailInput(element)
922    }
923}
924
925#[skip_serializing_none]
926#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
927pub struct SlackBlockRadioButtonsElement {
928    pub action_id: SlackActionId,
929    pub options: Vec<SlackBlockChoiceItem<SlackBlockText>>,
930    pub initial_option: Option<SlackBlockChoiceItem<SlackBlockText>>,
931    pub confirm: Option<SlackBlockConfirmItem>,
932    pub focus_on_load: Option<bool>,
933}
934
935impl From<SlackBlockRadioButtonsElement> for SlackSectionBlockElement {
936    fn from(element: SlackBlockRadioButtonsElement) -> Self {
937        SlackSectionBlockElement::RadioButtons(element)
938    }
939}
940
941impl From<SlackBlockRadioButtonsElement> for SlackInputBlockElement {
942    fn from(element: SlackBlockRadioButtonsElement) -> Self {
943        SlackInputBlockElement::RadioButtons(element)
944    }
945}
946
947impl From<SlackBlockRadioButtonsElement> for SlackActionBlockElement {
948    fn from(element: SlackBlockRadioButtonsElement) -> Self {
949        SlackActionBlockElement::RadioButtons(element)
950    }
951}
952
953#[skip_serializing_none]
954#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
955pub struct SlackBlockCheckboxesElement {
956    pub action_id: SlackActionId,
957    pub options: Vec<SlackBlockChoiceItem<SlackBlockText>>,
958    pub initial_options: Option<Vec<SlackBlockChoiceItem<SlackBlockText>>>,
959    pub confirm: Option<SlackBlockConfirmItem>,
960    pub focus_on_load: Option<bool>,
961}
962
963impl From<SlackBlockCheckboxesElement> for SlackSectionBlockElement {
964    fn from(element: SlackBlockCheckboxesElement) -> Self {
965        SlackSectionBlockElement::Checkboxes(element)
966    }
967}
968
969impl From<SlackBlockCheckboxesElement> for SlackInputBlockElement {
970    fn from(element: SlackBlockCheckboxesElement) -> Self {
971        SlackInputBlockElement::Checkboxes(element)
972    }
973}
974
975impl From<SlackBlockCheckboxesElement> for SlackActionBlockElement {
976    fn from(element: SlackBlockCheckboxesElement) -> Self {
977        SlackActionBlockElement::Checkboxes(element)
978    }
979}
980
981/**
982 * 'plain_text' type of https://api.slack.com/reference/block-kit/composition-objects#text
983 */
984#[skip_serializing_none]
985#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
986pub struct SlackBlockPlainText {
987    pub text: String,
988    pub emoji: Option<bool>,
989}
990
991/**
992 * 'mrkdwn' type of https://api.slack.com/reference/block-kit/composition-objects#text
993 */
994#[skip_serializing_none]
995#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
996pub struct SlackBlockMarkDownText {
997    pub text: String,
998    pub verbatim: Option<bool>,
999}
1000
1001/**
1002 * https://api.slack.com/reference/block-kit/composition-objects#text
1003 */
1004#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1005#[serde(tag = "type")]
1006pub enum SlackBlockText {
1007    #[serde(rename = "plain_text")]
1008    Plain(SlackBlockPlainText),
1009    #[serde(rename = "mrkdwn")]
1010    MarkDown(SlackBlockMarkDownText),
1011}
1012
1013#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1014#[serde(tag = "type", rename = "plain_text")]
1015pub struct SlackBlockPlainTextOnly {
1016    #[serde(flatten)]
1017    value: SlackBlockPlainText,
1018}
1019
1020impl SlackBlockPlainText {
1021    pub fn as_block_text(&self) -> SlackBlockText {
1022        SlackBlockText::Plain(self.clone())
1023    }
1024}
1025
1026impl From<String> for SlackBlockPlainText {
1027    fn from(value: String) -> Self {
1028        SlackBlockPlainText::new(value)
1029    }
1030}
1031
1032impl From<&str> for SlackBlockPlainText {
1033    fn from(value: &str) -> Self {
1034        SlackBlockPlainText::new(String::from(value))
1035    }
1036}
1037
1038impl SlackBlockMarkDownText {
1039    pub fn as_block_text(&self) -> SlackBlockText {
1040        SlackBlockText::MarkDown(self.clone())
1041    }
1042}
1043
1044impl From<String> for SlackBlockMarkDownText {
1045    fn from(value: String) -> Self {
1046        SlackBlockMarkDownText::new(value)
1047    }
1048}
1049
1050impl From<&str> for SlackBlockMarkDownText {
1051    fn from(value: &str) -> Self {
1052        SlackBlockMarkDownText::new(String::from(value))
1053    }
1054}
1055
1056impl From<SlackBlockPlainText> for SlackBlockPlainTextOnly {
1057    fn from(pt: SlackBlockPlainText) -> Self {
1058        SlackBlockPlainTextOnly { value: pt }
1059    }
1060}
1061
1062impl From<SlackBlockPlainText> for SlackBlockText {
1063    fn from(text: SlackBlockPlainText) -> Self {
1064        SlackBlockText::Plain(text)
1065    }
1066}
1067
1068impl From<SlackBlockMarkDownText> for SlackBlockText {
1069    fn from(text: SlackBlockMarkDownText) -> Self {
1070        SlackBlockText::MarkDown(text)
1071    }
1072}
1073
1074impl From<SlackBlockPlainText> for SlackContextBlockElement {
1075    fn from(text: SlackBlockPlainText) -> Self {
1076        SlackContextBlockElement::Plain(text)
1077    }
1078}
1079
1080impl From<SlackBlockMarkDownText> for SlackContextBlockElement {
1081    fn from(text: SlackBlockMarkDownText) -> Self {
1082        SlackContextBlockElement::MarkDown(text)
1083    }
1084}
1085
1086impl From<SlackBlockPlainTextOnly> for SlackBlockText {
1087    fn from(text: SlackBlockPlainTextOnly) -> Self {
1088        SlackBlockText::Plain(text.value)
1089    }
1090}
1091
1092impl From<String> for SlackBlockPlainTextOnly {
1093    fn from(value: String) -> Self {
1094        SlackBlockPlainTextOnly {
1095            value: value.into(),
1096        }
1097    }
1098}
1099
1100impl From<&str> for SlackBlockPlainTextOnly {
1101    fn from(value: &str) -> Self {
1102        SlackBlockPlainTextOnly {
1103            value: value.into(),
1104        }
1105    }
1106}
1107
1108/**
1109 * https://api.slack.com/reference/block-kit/blocks#video
1110 */
1111#[skip_serializing_none]
1112#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1113pub struct SlackVideoBlock {
1114    pub alt_text: String,
1115    pub author_name: Option<String>,
1116    pub block_id: Option<SlackBlockId>,
1117    pub description: Option<SlackBlockPlainTextOnly>,
1118    pub provider_icon_url: Option<Url>,
1119    pub provider_name: Option<String>,
1120    pub title: SlackBlockPlainTextOnly,
1121    pub title_url: Option<Url>,
1122    pub thumbnail_url: Url,
1123    pub video_url: Url,
1124}
1125
1126impl From<SlackVideoBlock> for SlackBlock {
1127    fn from(block: SlackVideoBlock) -> Self {
1128        SlackBlock::Video(block)
1129    }
1130}
1131
1132/**
1133 * https://api.slack.com/reference/block-kit/blocks#markdown
1134 */
1135#[skip_serializing_none]
1136#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1137pub struct SlackMarkdownBlock {
1138    pub block_id: Option<SlackBlockId>,
1139    pub text: String,
1140}
1141
1142impl From<SlackMarkdownBlock> for SlackBlock {
1143    fn from(block: SlackMarkdownBlock) -> Self {
1144        SlackBlock::Markdown(block)
1145    }
1146}
1147
1148/**
1149 * https://api.slack.com/reference/block-kit/blocks#rich_text
1150 */
1151#[skip_serializing_none]
1152#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1153pub struct SlackRichTextBlock {
1154    pub block_id: Option<SlackBlockId>,
1155    pub elements: Vec<SlackRichTextElement>,
1156}
1157
1158impl From<SlackRichTextBlock> for SlackBlock {
1159    fn from(block: SlackRichTextBlock) -> Self {
1160        SlackBlock::RichText(block)
1161    }
1162}
1163
1164#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1165#[serde(tag = "type", rename_all = "snake_case")]
1166pub enum SlackRichTextInlineContent {
1167    #[serde(rename = "rich_text")]
1168    RichText(SlackRichTextBlock),
1169}
1170
1171impl From<SlackRichTextBlock> for SlackRichTextInlineContent {
1172    fn from(block: SlackRichTextBlock) -> Self {
1173        SlackRichTextInlineContent::RichText(block)
1174    }
1175}
1176
1177#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1178#[serde(tag = "type")]
1179pub enum SlackRichTextElement {
1180    #[serde(rename = "rich_text_section")]
1181    Section(SlackRichTextSection),
1182    #[serde(rename = "rich_text_list")]
1183    List(SlackRichTextList),
1184    #[serde(rename = "rich_text_preformatted")]
1185    Preformatted(SlackRichTextPreformatted),
1186    #[serde(rename = "rich_text_quote")]
1187    Quote(SlackRichTextQuote),
1188}
1189
1190impl From<SlackRichTextSection> for SlackRichTextElement {
1191    fn from(element: SlackRichTextSection) -> Self {
1192        SlackRichTextElement::Section(element)
1193    }
1194}
1195
1196impl From<SlackRichTextList> for SlackRichTextElement {
1197    fn from(list: SlackRichTextList) -> Self {
1198        SlackRichTextElement::List(list)
1199    }
1200}
1201
1202impl From<SlackRichTextPreformatted> for SlackRichTextElement {
1203    fn from(element: SlackRichTextPreformatted) -> Self {
1204        SlackRichTextElement::Preformatted(element)
1205    }
1206}
1207
1208impl From<SlackRichTextQuote> for SlackRichTextElement {
1209    fn from(element: SlackRichTextQuote) -> Self {
1210        SlackRichTextElement::Quote(element)
1211    }
1212}
1213
1214#[skip_serializing_none]
1215#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1216pub struct SlackRichTextSection {
1217    pub elements: Vec<SlackRichTextInlineElement>,
1218}
1219
1220/// A bare string becomes a section holding a single unstyled text run.
1221impl From<&str> for SlackRichTextSection {
1222    fn from(value: &str) -> Self {
1223        SlackRichTextSection::new(vec![value.into()])
1224    }
1225}
1226
1227impl From<String> for SlackRichTextSection {
1228    fn from(value: String) -> Self {
1229        SlackRichTextSection::new(vec![value.into()])
1230    }
1231}
1232
1233#[skip_serializing_none]
1234#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1235pub struct SlackRichTextList {
1236    pub style: SlackRichTextListStyle,
1237    pub elements: Vec<SlackRichTextListElement>,
1238    pub indent: Option<u64>,
1239    pub offset: Option<u64>,
1240    pub border: Option<u64>,
1241}
1242
1243#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1244#[serde(tag = "type")]
1245pub enum SlackRichTextListElement {
1246    #[serde(rename = "rich_text_section")]
1247    Section(SlackRichTextSection),
1248}
1249
1250impl From<SlackRichTextSection> for SlackRichTextListElement {
1251    fn from(element: SlackRichTextSection) -> Self {
1252        SlackRichTextListElement::Section(element)
1253    }
1254}
1255
1256/// A bare string becomes a single-run section, the same as `SlackRichTextSection::from`.
1257impl From<&str> for SlackRichTextListElement {
1258    fn from(value: &str) -> Self {
1259        SlackRichTextListElement::Section(value.into())
1260    }
1261}
1262
1263impl From<String> for SlackRichTextListElement {
1264    fn from(value: String) -> Self {
1265        SlackRichTextListElement::Section(value.into())
1266    }
1267}
1268
1269#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1270#[serde(rename_all = "snake_case")]
1271pub enum SlackRichTextListStyle {
1272    Bullet,
1273    Ordered,
1274}
1275
1276#[skip_serializing_none]
1277#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1278pub struct SlackRichTextPreformatted {
1279    pub elements: Vec<SlackRichTextInlineElement>,
1280    pub border: Option<u64>,
1281    pub language: Option<String>,
1282}
1283
1284#[skip_serializing_none]
1285#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1286pub struct SlackRichTextQuote {
1287    pub elements: Vec<SlackRichTextInlineElement>,
1288    pub border: Option<u64>,
1289}
1290
1291#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1292#[serde(tag = "type")]
1293pub enum SlackRichTextInlineElement {
1294    #[serde(rename = "text")]
1295    Text(SlackRichTextText),
1296    #[serde(rename = "link")]
1297    Link(SlackRichTextLink),
1298    #[serde(rename = "user")]
1299    User(SlackRichTextUser),
1300    #[serde(rename = "channel")]
1301    Channel(SlackRichTextChannel),
1302    #[serde(rename = "usergroup")]
1303    UserGroup(SlackRichTextUserGroup),
1304    #[serde(rename = "emoji")]
1305    Emoji(SlackRichTextEmoji),
1306    #[serde(rename = "date")]
1307    Date(SlackRichTextDate),
1308    #[serde(rename = "broadcast")]
1309    Broadcast(SlackRichTextBroadcast),
1310    #[serde(rename = "color")]
1311    Color(SlackRichTextColor),
1312    #[serde(rename = "message_mention")]
1313    MessageMention(SlackRichTextMessageMention),
1314    #[serde(untagged)]
1315    Unknown(serde_json::Value),
1316}
1317
1318/// A bare string becomes an unstyled text run.
1319impl From<&str> for SlackRichTextInlineElement {
1320    fn from(value: &str) -> Self {
1321        SlackRichTextInlineElement::Text(SlackRichTextText::new(value.to_string()))
1322    }
1323}
1324
1325impl From<String> for SlackRichTextInlineElement {
1326    fn from(value: String) -> Self {
1327        SlackRichTextInlineElement::Text(SlackRichTextText::new(value))
1328    }
1329}
1330
1331impl From<SlackRichTextText> for SlackRichTextInlineElement {
1332    fn from(element: SlackRichTextText) -> Self {
1333        SlackRichTextInlineElement::Text(element)
1334    }
1335}
1336
1337impl From<SlackRichTextLink> for SlackRichTextInlineElement {
1338    fn from(element: SlackRichTextLink) -> Self {
1339        SlackRichTextInlineElement::Link(element)
1340    }
1341}
1342
1343impl From<SlackRichTextUser> for SlackRichTextInlineElement {
1344    fn from(element: SlackRichTextUser) -> Self {
1345        SlackRichTextInlineElement::User(element)
1346    }
1347}
1348
1349impl From<SlackRichTextChannel> for SlackRichTextInlineElement {
1350    fn from(element: SlackRichTextChannel) -> Self {
1351        SlackRichTextInlineElement::Channel(element)
1352    }
1353}
1354
1355impl From<SlackRichTextUserGroup> for SlackRichTextInlineElement {
1356    fn from(element: SlackRichTextUserGroup) -> Self {
1357        SlackRichTextInlineElement::UserGroup(element)
1358    }
1359}
1360
1361impl From<SlackRichTextEmoji> for SlackRichTextInlineElement {
1362    fn from(element: SlackRichTextEmoji) -> Self {
1363        SlackRichTextInlineElement::Emoji(element)
1364    }
1365}
1366
1367impl From<SlackRichTextDate> for SlackRichTextInlineElement {
1368    fn from(element: SlackRichTextDate) -> Self {
1369        SlackRichTextInlineElement::Date(element)
1370    }
1371}
1372
1373impl From<SlackRichTextBroadcast> for SlackRichTextInlineElement {
1374    fn from(element: SlackRichTextBroadcast) -> Self {
1375        SlackRichTextInlineElement::Broadcast(element)
1376    }
1377}
1378
1379impl From<SlackRichTextColor> for SlackRichTextInlineElement {
1380    fn from(element: SlackRichTextColor) -> Self {
1381        SlackRichTextInlineElement::Color(element)
1382    }
1383}
1384
1385impl From<SlackRichTextMessageMention> for SlackRichTextInlineElement {
1386    fn from(element: SlackRichTextMessageMention) -> Self {
1387        SlackRichTextInlineElement::MessageMention(element)
1388    }
1389}
1390
1391#[skip_serializing_none]
1392#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1393pub struct SlackRichTextStyle {
1394    pub bold: Option<bool>,
1395    pub italic: Option<bool>,
1396    pub strike: Option<bool>,
1397    pub code: Option<bool>,
1398    pub underline: Option<bool>,
1399    pub highlight: Option<bool>,
1400    pub client_highlight: Option<bool>,
1401    pub unlink: Option<bool>,
1402}
1403
1404#[skip_serializing_none]
1405#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1406pub struct SlackRichTextText {
1407    pub text: String,
1408    pub style: Option<SlackRichTextStyle>,
1409}
1410
1411impl SlackRichTextText {
1412    /// Sets the bold flag, preserving any other flags already set on `style`.
1413    pub fn bold(mut self) -> Self {
1414        self.style.get_or_insert_with(SlackRichTextStyle::new).bold = Some(true);
1415        self
1416    }
1417
1418    /// Sets the italic flag, preserving any other flags already set on `style`.
1419    pub fn italic(mut self) -> Self {
1420        self.style
1421            .get_or_insert_with(SlackRichTextStyle::new)
1422            .italic = Some(true);
1423        self
1424    }
1425
1426    /// Sets the strike flag, preserving any other flags already set on `style`.
1427    pub fn strike(mut self) -> Self {
1428        self.style
1429            .get_or_insert_with(SlackRichTextStyle::new)
1430            .strike = Some(true);
1431        self
1432    }
1433
1434    /// Sets the code flag, preserving any other flags already set on `style`.
1435    pub fn code(mut self) -> Self {
1436        self.style.get_or_insert_with(SlackRichTextStyle::new).code = Some(true);
1437        self
1438    }
1439}
1440
1441#[skip_serializing_none]
1442#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1443pub struct SlackRichTextLink {
1444    pub url: SlackRelaxedUrl,
1445    pub text: Option<String>,
1446    #[serde(rename = "unsafe")]
1447    pub unsafe_: Option<bool>,
1448    pub style: Option<SlackRichTextStyle>,
1449}
1450
1451#[skip_serializing_none]
1452#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1453pub struct SlackRichTextUser {
1454    pub user_id: SlackUserId,
1455    pub style: Option<SlackRichTextStyle>,
1456}
1457
1458#[skip_serializing_none]
1459#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1460pub struct SlackRichTextChannel {
1461    pub channel_id: SlackChannelId,
1462    pub style: Option<SlackRichTextStyle>,
1463}
1464
1465#[skip_serializing_none]
1466#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1467pub struct SlackRichTextUserGroup {
1468    pub usergroup_id: SlackUserGroupId,
1469    pub style: Option<SlackRichTextStyle>,
1470}
1471
1472#[skip_serializing_none]
1473#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1474pub struct SlackRichTextEmoji {
1475    pub name: SlackEmojiName,
1476    pub unicode: Option<String>,
1477}
1478
1479#[skip_serializing_none]
1480#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1481pub struct SlackRichTextDate {
1482    pub timestamp: SlackDateTime,
1483    pub format: String,
1484    pub fallback: Option<String>,
1485    pub style: Option<SlackRichTextStyle>,
1486}
1487
1488#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1489#[serde(rename_all = "snake_case")]
1490pub enum SlackRichTextBroadcastRange {
1491    Here,
1492    Channel,
1493    Everyone,
1494}
1495
1496#[skip_serializing_none]
1497#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1498pub struct SlackRichTextBroadcast {
1499    pub range: SlackRichTextBroadcastRange,
1500    pub style: Option<SlackRichTextStyle>,
1501}
1502
1503#[skip_serializing_none]
1504#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1505pub struct SlackRichTextColor {
1506    pub value: String,
1507}
1508
1509#[skip_serializing_none]
1510#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1511pub struct SlackRichTextMessageMention {
1512    pub url: SlackRelaxedUrl,
1513    pub text: Option<String>,
1514    pub channel_id: Option<SlackChannelId>,
1515    pub author_id: Option<SlackUserId>,
1516    pub message_ts: Option<SlackTs>,
1517    pub thread_ts: Option<SlackTs>,
1518    pub style: Option<SlackRichTextStyle>,
1519}
1520
1521/**
1522 * https://api.slack.com/reference/block-kit/block-elements#rich_text_input
1523 */
1524#[skip_serializing_none]
1525#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1526pub struct SlackBlockRichTextInputElement {
1527    pub action_id: SlackActionId,
1528    pub initial_value: Option<SlackRichTextBlock>,
1529    pub focus_on_load: Option<bool>,
1530    pub placeholder: Option<SlackBlockPlainTextOnly>,
1531}
1532
1533impl From<SlackBlockRichTextInputElement> for SlackInputBlockElement {
1534    fn from(element: SlackBlockRichTextInputElement) -> Self {
1535        SlackInputBlockElement::RichTextInput(element)
1536    }
1537}
1538
1539#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1540#[serde(untagged)]
1541pub enum SlackImageUrlOrFile {
1542    ImageUrl { image_url: Url },
1543    SlackFile { slack_file: SlackFileIdOrUrl },
1544}
1545
1546impl SlackImageUrlOrFile {
1547    pub fn image_url(&self) -> Option<&Url> {
1548        match self {
1549            SlackImageUrlOrFile::ImageUrl { image_url } => Some(image_url),
1550            SlackImageUrlOrFile::SlackFile { slack_file } => match slack_file {
1551                SlackFileIdOrUrl::Url { url } => Some(url),
1552                _ => None,
1553            },
1554        }
1555    }
1556}
1557
1558impl From<Url> for SlackImageUrlOrFile {
1559    fn from(value: Url) -> Self {
1560        SlackImageUrlOrFile::ImageUrl { image_url: value }
1561    }
1562}
1563
1564#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1565#[serde(untagged)]
1566pub enum SlackFileIdOrUrl {
1567    Id { id: SlackFileId },
1568    Url { url: Url },
1569}
1570
1571impl From<SlackFileId> for SlackFileIdOrUrl {
1572    fn from(value: SlackFileId) -> Self {
1573        SlackFileIdOrUrl::Id { id: value }
1574    }
1575}
1576
1577/**
1578 * https://docs.slack.dev/reference/block-kit/blocks/table-block
1579 */
1580#[skip_serializing_none]
1581#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1582pub struct SlackTableBlock {
1583    pub block_id: Option<SlackBlockId>,
1584    pub rows: Vec<Vec<SlackTableCell>>,
1585    pub column_settings: Option<Vec<SlackTableColumnSetting>>,
1586}
1587
1588impl From<SlackTableBlock> for SlackBlock {
1589    fn from(block: SlackTableBlock) -> Self {
1590        SlackBlock::Table(block)
1591    }
1592}
1593
1594#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1595#[serde(tag = "type")]
1596pub enum SlackTableCell {
1597    #[serde(rename = "raw_text")]
1598    RawText(SlackTableRawTextCell),
1599    #[serde(rename = "rich_text")]
1600    RichText(SlackTableRichTextCell),
1601}
1602
1603/// A bare string becomes a raw-text cell.
1604impl From<&str> for SlackTableCell {
1605    fn from(value: &str) -> Self {
1606        SlackTableCell::RawText(SlackTableRawTextCell::new(value.to_string()))
1607    }
1608}
1609
1610impl From<String> for SlackTableCell {
1611    fn from(value: String) -> Self {
1612        SlackTableCell::RawText(SlackTableRawTextCell::new(value))
1613    }
1614}
1615
1616impl From<SlackTableRawTextCell> for SlackTableCell {
1617    fn from(cell: SlackTableRawTextCell) -> Self {
1618        SlackTableCell::RawText(cell)
1619    }
1620}
1621
1622impl From<SlackTableRichTextCell> for SlackTableCell {
1623    fn from(cell: SlackTableRichTextCell) -> Self {
1624        SlackTableCell::RichText(cell)
1625    }
1626}
1627
1628#[skip_serializing_none]
1629#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1630pub struct SlackTableRawTextCell {
1631    pub text: String,
1632}
1633
1634#[skip_serializing_none]
1635#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1636pub struct SlackTableRichTextCell {
1637    pub elements: Vec<SlackRichTextElement>,
1638}
1639
1640#[skip_serializing_none]
1641#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1642pub struct SlackTableColumnSetting {
1643    pub align: Option<SlackTableColumnAlign>,
1644    pub is_wrapped: Option<bool>,
1645}
1646
1647#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1648#[serde(rename_all = "snake_case")]
1649pub enum SlackTableColumnAlign {
1650    Left,
1651    Center,
1652    Right,
1653}
1654
1655/**
1656 * https://docs.slack.dev/reference/block-kit/blocks/task-card-block
1657 */
1658#[skip_serializing_none]
1659#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1660pub struct SlackTaskCardBlock {
1661    pub task_id: SlackTaskId,
1662    pub title: String,
1663    pub block_id: Option<SlackBlockId>,
1664    pub status: Option<SlackTaskCardStatus>,
1665    pub icon: Option<SlackTaskCardIcon>,
1666    pub hide_title: Option<bool>,
1667    #[serde(rename = "details")]
1668    pub details: Option<SlackRichTextInlineContent>,
1669    #[serde(rename = "output")]
1670    pub output: Option<SlackRichTextInlineContent>,
1671    pub sources: Option<Vec<SlackTaskCardSource>>,
1672}
1673
1674impl From<SlackTaskCardBlock> for SlackBlock {
1675    fn from(block: SlackTaskCardBlock) -> Self {
1676        SlackBlock::TaskCard(block)
1677    }
1678}
1679
1680#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1681#[serde(rename_all = "snake_case")]
1682pub enum SlackTaskCardStatus {
1683    Pending,
1684    InProgress,
1685    Complete,
1686    Error,
1687}
1688
1689/// Icon shown on a task card, serialised as `{"type":"icon","name":"<icon name>"}`.
1690/// `name` is an icon name, not a URL.
1691#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1692#[serde(tag = "type", rename = "icon")]
1693pub struct SlackTaskCardIcon {
1694    pub name: String,
1695}
1696
1697/**
1698 * https://docs.slack.dev/reference/block-kit/block-elements/url-source-element
1699 */
1700#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1701pub struct SlackUrlSourceElement {
1702    pub url: Url,
1703    pub text: String,
1704}
1705
1706#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1707#[serde(tag = "type")]
1708pub enum SlackTaskCardSource {
1709    #[serde(rename = "url")]
1710    Url(SlackUrlSourceElement),
1711}
1712
1713impl From<SlackUrlSourceElement> for SlackTaskCardSource {
1714    fn from(element: SlackUrlSourceElement) -> Self {
1715        SlackTaskCardSource::Url(element)
1716    }
1717}
1718
1719/**
1720 * https://docs.slack.dev/reference/block-kit/block-elements/file-input-element
1721 */
1722#[skip_serializing_none]
1723#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1724pub struct SlackBlockFileInputElement {
1725    pub action_id: SlackActionId,
1726    pub filetypes: Option<Vec<String>>,
1727    pub max_files: Option<u64>,
1728}
1729
1730impl From<SlackBlockFileInputElement> for SlackInputBlockElement {
1731    fn from(element: SlackBlockFileInputElement) -> Self {
1732        SlackInputBlockElement::FileInput(element)
1733    }
1734}
1735
1736/**
1737 * https://docs.slack.dev/reference/block-kit/blocks/alert-block
1738 */
1739#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1740#[serde(rename_all = "snake_case")]
1741pub enum SlackAlertLevel {
1742    Warning,
1743    Error,
1744    Info,
1745    Success,
1746}
1747
1748#[skip_serializing_none]
1749#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1750pub struct SlackAlertBlock {
1751    pub block_id: Option<SlackBlockId>,
1752    pub text: SlackBlockText,
1753    pub level: Option<SlackAlertLevel>,
1754}
1755
1756impl From<SlackAlertBlock> for SlackBlock {
1757    fn from(block: SlackAlertBlock) -> Self {
1758        SlackBlock::Alert(block)
1759    }
1760}
1761
1762/**
1763 * https://docs.slack.dev/reference/block-kit/blocks/card-block
1764 */
1765#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1766#[serde(tag = "type")]
1767pub enum SlackCardImageElement {
1768    #[serde(rename = "image")]
1769    Image(SlackBlockImageElement),
1770}
1771
1772impl From<SlackBlockImageElement> for SlackCardImageElement {
1773    fn from(element: SlackBlockImageElement) -> Self {
1774        SlackCardImageElement::Image(element)
1775    }
1776}
1777
1778#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1779#[serde(tag = "type")]
1780pub enum SlackCardActionBlockElement {
1781    #[serde(rename = "button")]
1782    Button(SlackBlockButtonElement),
1783}
1784
1785impl From<SlackBlockButtonElement> for SlackCardActionBlockElement {
1786    fn from(element: SlackBlockButtonElement) -> Self {
1787        SlackCardActionBlockElement::Button(element)
1788    }
1789}
1790
1791#[skip_serializing_none]
1792#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1793pub struct SlackCardBlock {
1794    pub block_id: Option<SlackBlockId>,
1795    pub title: Option<SlackBlockText>,
1796    pub subtitle: Option<SlackBlockText>,
1797    pub body: Option<SlackBlockText>,
1798    pub hero_image: Option<SlackCardImageElement>,
1799    pub icon: Option<SlackCardImageElement>,
1800    pub actions: Option<Vec<SlackCardActionBlockElement>>,
1801}
1802
1803impl From<SlackCardBlock> for SlackBlock {
1804    fn from(block: SlackCardBlock) -> Self {
1805        SlackBlock::Card(block)
1806    }
1807}
1808
1809/**
1810 * https://docs.slack.dev/reference/block-kit/blocks/carousel-block
1811 */
1812#[skip_serializing_none]
1813#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1814pub struct SlackCarouselBlock {
1815    pub block_id: Option<SlackBlockId>,
1816    pub elements: Vec<SlackBlock>,
1817}
1818
1819impl From<SlackCarouselBlock> for SlackBlock {
1820    fn from(block: SlackCarouselBlock) -> Self {
1821        SlackBlock::Carousel(block)
1822    }
1823}
1824
1825/**
1826 * https://docs.slack.dev/reference/block-kit/blocks/context-actions-block
1827 * https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element
1828 * https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element
1829 */
1830#[skip_serializing_none]
1831#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1832pub struct SlackFeedbackButtonItem {
1833    pub action_id: SlackActionId,
1834    pub value: String,
1835    pub text: SlackBlockPlainTextOnly,
1836    pub confirm: Option<SlackBlockConfirmItem>,
1837}
1838
1839#[skip_serializing_none]
1840#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1841pub struct SlackBlockFeedbackButtonsElement {
1842    pub action_id: SlackActionId,
1843    pub positive: SlackFeedbackButtonItem,
1844    pub negative: SlackFeedbackButtonItem,
1845}
1846
1847#[skip_serializing_none]
1848#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1849pub struct SlackBlockIconButtonElement {
1850    pub action_id: SlackActionId,
1851    pub icon: String,
1852    pub text: SlackBlockPlainTextOnly,
1853    pub value: Option<String>,
1854    pub confirm: Option<SlackBlockConfirmItem>,
1855    pub accessibility_label: Option<SlackAccessibilityLabel>,
1856    pub visible_to_user_ids: Option<Vec<SlackUserId>>,
1857}
1858
1859#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1860#[serde(tag = "type")]
1861pub enum SlackContextActionBlockElement {
1862    #[serde(rename = "feedback_buttons")]
1863    FeedbackButtons(SlackBlockFeedbackButtonsElement),
1864    #[serde(rename = "icon_button")]
1865    IconButton(SlackBlockIconButtonElement),
1866}
1867
1868impl From<SlackBlockFeedbackButtonsElement> for SlackContextActionBlockElement {
1869    fn from(element: SlackBlockFeedbackButtonsElement) -> Self {
1870        SlackContextActionBlockElement::FeedbackButtons(element)
1871    }
1872}
1873
1874impl From<SlackBlockIconButtonElement> for SlackContextActionBlockElement {
1875    fn from(element: SlackBlockIconButtonElement) -> Self {
1876        SlackContextActionBlockElement::IconButton(element)
1877    }
1878}
1879
1880#[skip_serializing_none]
1881#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1882pub struct SlackContextActionsBlock {
1883    pub block_id: Option<SlackBlockId>,
1884    pub elements: Vec<SlackContextActionBlockElement>,
1885}
1886
1887impl From<SlackContextActionsBlock> for SlackBlock {
1888    fn from(block: SlackContextActionsBlock) -> Self {
1889        SlackBlock::ContextActions(block)
1890    }
1891}
1892
1893/**
1894 * https://docs.slack.dev/reference/block-kit/blocks/container-block
1895 *
1896 * One of `title` / `rich_text_title` is required; `rich_text_title` wins when
1897 * both are set. `child_blocks` holds at most 10 blocks. The docs list
1898 * `actions, context, divider, file, header, image, input, rich_text, section,
1899 * table, video` as supported children while the live validator accepts
1900 * `divider, image, section, contact_card, callout, canvas_table, actions,
1901 * video, header, context, table, layout`; neither set is enforced here.
1902 */
1903#[skip_serializing_none]
1904#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1905pub struct SlackContainerBlock {
1906    pub block_id: Option<SlackBlockId>,
1907    pub title: Option<SlackBlockPlainTextOnly>,
1908    pub rich_text_title: Option<SlackRichTextInlineContent>,
1909    pub subtitle: Option<SlackBlockText>,
1910    pub child_blocks: Vec<SlackBlock>,
1911    pub width: Option<SlackContainerWidth>,
1912    pub icon: Option<SlackCardImageElement>,
1913    pub is_collapsible: Option<bool>,
1914    pub default_collapsed: Option<bool>,
1915    pub has_header_divider: Option<bool>,
1916}
1917
1918impl From<SlackContainerBlock> for SlackBlock {
1919    fn from(block: SlackContainerBlock) -> Self {
1920        SlackBlock::Container(block)
1921    }
1922}
1923
1924/// `narrow`, `standard` (default) and `wide` are platform-constrained widths;
1925/// `full` fills the available space.
1926#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1927#[serde(rename_all = "snake_case")]
1928pub enum SlackContainerWidth {
1929    Narrow,
1930    Standard,
1931    Wide,
1932    Full,
1933}
1934
1935/**
1936 * https://docs.slack.dev/reference/block-kit/blocks/plan-block
1937 *
1938 * `tasks` holds up to 50 task-card objects without a `type` tag
1939 * (`SlackTaskCardBlock` serialises without one); every `task_id` in a plan
1940 * must be unique.
1941 */
1942#[skip_serializing_none]
1943#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1944pub struct SlackPlanBlock {
1945    pub title: String,
1946    pub tasks: Vec<SlackTaskCardBlock>,
1947    pub block_id: Option<SlackBlockId>,
1948}
1949
1950impl From<SlackPlanBlock> for SlackBlock {
1951    fn from(block: SlackPlanBlock) -> Self {
1952        SlackBlock::Plan(block)
1953    }
1954}
1955
1956/**
1957 * https://docs.slack.dev/reference/block-kit/blocks/data-table-block
1958 *
1959 * The first row is the header and may not hold `rich_text` cells. 2–201 rows,
1960 * 1–20 columns, every row the same length; `page_size` is 1–100 (default 5).
1961 */
1962#[skip_serializing_none]
1963#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1964pub struct SlackDataTableBlock {
1965    pub caption: String,
1966    pub rows: Vec<Vec<SlackDataTableCell>>,
1967    pub block_id: Option<SlackBlockId>,
1968    pub page_size: Option<u32>,
1969    pub row_header_column_index: Option<u32>,
1970}
1971
1972impl From<SlackDataTableBlock> for SlackBlock {
1973    fn from(block: SlackDataTableBlock) -> Self {
1974        SlackBlock::DataTable(block)
1975    }
1976}
1977
1978#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1979#[serde(tag = "type")]
1980pub enum SlackDataTableCell {
1981    #[serde(rename = "raw_text")]
1982    RawText(SlackTableRawTextCell),
1983    #[serde(rename = "raw_number")]
1984    RawNumber(SlackDataTableRawNumberCell),
1985    #[serde(rename = "rich_text")]
1986    RichText(SlackTableRichTextCell),
1987    #[serde(rename = "action_cell")]
1988    ActionCell(SlackDataTableActionCell),
1989    #[serde(untagged)]
1990    Unknown(serde_json::Value),
1991}
1992
1993/// A bare string becomes a raw-text cell.
1994impl From<&str> for SlackDataTableCell {
1995    fn from(value: &str) -> Self {
1996        SlackDataTableCell::RawText(SlackTableRawTextCell::new(value.to_string()))
1997    }
1998}
1999
2000impl From<String> for SlackDataTableCell {
2001    fn from(value: String) -> Self {
2002        SlackDataTableCell::RawText(SlackTableRawTextCell::new(value))
2003    }
2004}
2005
2006impl From<SlackTableRawTextCell> for SlackDataTableCell {
2007    fn from(cell: SlackTableRawTextCell) -> Self {
2008        SlackDataTableCell::RawText(cell)
2009    }
2010}
2011
2012impl From<SlackDataTableRawNumberCell> for SlackDataTableCell {
2013    fn from(cell: SlackDataTableRawNumberCell) -> Self {
2014        SlackDataTableCell::RawNumber(cell)
2015    }
2016}
2017
2018impl From<SlackTableRichTextCell> for SlackDataTableCell {
2019    fn from(cell: SlackTableRichTextCell) -> Self {
2020        SlackDataTableCell::RichText(cell)
2021    }
2022}
2023
2024impl From<SlackDataTableActionCell> for SlackDataTableCell {
2025    fn from(cell: SlackDataTableActionCell) -> Self {
2026        SlackDataTableCell::ActionCell(cell)
2027    }
2028}
2029
2030/// `value` sorts the column numerically when every cell in it is a
2031/// `raw_number`; `text` is what is displayed.
2032#[skip_serializing_none]
2033#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2034pub struct SlackDataTableRawNumberCell {
2035    pub value: serde_json::Number,
2036    pub text: String,
2037}
2038
2039/// A button in a cell; `fallback` is a `raw_text` or `raw_number` cell shown
2040/// by clients that do not support action cells.
2041#[skip_serializing_none]
2042#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2043pub struct SlackDataTableActionCell {
2044    pub element: SlackDataTableActionElement,
2045    pub fallback: Option<Box<SlackDataTableCell>>,
2046}
2047
2048#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
2049#[serde(tag = "type")]
2050pub enum SlackDataTableActionElement {
2051    #[serde(rename = "button")]
2052    Button(SlackBlockButtonElement),
2053}
2054
2055impl From<SlackBlockButtonElement> for SlackDataTableActionElement {
2056    fn from(element: SlackBlockButtonElement) -> Self {
2057        SlackDataTableActionElement::Button(element)
2058    }
2059}
2060
2061/**
2062 * https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block
2063 *
2064 * `title` is at most 50 characters; a message may hold at most two of these.
2065 */
2066#[skip_serializing_none]
2067#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2068pub struct SlackDataVisualizationBlock {
2069    pub title: String,
2070    pub chart: SlackChart,
2071    pub block_id: Option<SlackBlockId>,
2072}
2073
2074impl From<SlackDataVisualizationBlock> for SlackBlock {
2075    fn from(block: SlackDataVisualizationBlock) -> Self {
2076        SlackBlock::DataVisualization(block)
2077    }
2078}
2079
2080#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
2081#[serde(tag = "type", rename_all = "snake_case")]
2082pub enum SlackChart {
2083    Pie(SlackPieChart),
2084    Bar(SlackSeriesChart),
2085    Area(SlackSeriesChart),
2086    Line(SlackSeriesChart),
2087}
2088
2089/// 1–12 segments; each renders as `value / sum(values)`.
2090#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2091pub struct SlackPieChart {
2092    pub segments: Vec<SlackChartSegment>,
2093}
2094
2095/// Shared by `bar`, `area` and `line`: 1–12 series, each with exactly one data
2096/// point per entry of `axis_config.categories`, and unique series names.
2097#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2098pub struct SlackSeriesChart {
2099    pub series: Vec<SlackChartDataSeries>,
2100    pub axis_config: SlackChartAxisConfig,
2101}
2102
2103#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2104pub struct SlackChartSegment {
2105    pub label: String,
2106    pub value: serde_json::Number,
2107}
2108
2109#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2110pub struct SlackChartDataSeries {
2111    pub name: String,
2112    pub data: Vec<SlackChartDataPoint>,
2113}
2114
2115#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2116pub struct SlackChartDataPoint {
2117    pub label: String,
2118    pub value: serde_json::Number,
2119}
2120
2121/// `categories` defines the valid data-point labels and their x-axis order.
2122#[skip_serializing_none]
2123#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2124pub struct SlackChartAxisConfig {
2125    pub categories: Vec<String>,
2126    pub x_label: Option<String>,
2127    pub y_label: Option<String>,
2128}
2129
2130#[cfg(test)]
2131mod test {
2132    use super::*;
2133    use crate::blocks::SlackHomeView;
2134
2135    #[test]
2136    fn test_conversation_filter_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2137        let payload = include_str!("./fixtures/slack_conversations_select_with_filter.json");
2138        let block: SlackBlock = serde_json::from_str(payload)?;
2139        match block {
2140            SlackBlock::Section(section) => match section.accessory {
2141                Some(SlackSectionBlockElement::ConversationsSelect(elem)) => {
2142                    let filter = elem.filter.expect("filter should be present");
2143                    let include = filter.include.expect("include should be present");
2144                    assert_eq!(include.len(), 2);
2145                    assert_eq!(include[0], SlackConversationFilterInclude::Public);
2146                    assert_eq!(include[1], SlackConversationFilterInclude::Private);
2147                    assert_eq!(filter.exclude_external_shared_channels, Some(true));
2148                    assert_eq!(filter.exclude_bot_users, Some(true));
2149                }
2150                _ => panic!("Expected ConversationsSelect accessory"),
2151            },
2152            _ => panic!("Expected Section block"),
2153        }
2154        Ok(())
2155    }
2156
2157    #[test]
2158    fn test_conversation_filter_serialize() -> Result<(), Box<dyn std::error::Error>> {
2159        let filter = SlackBlockConversationFilter::new()
2160            .with_include(vec![
2161                SlackConversationFilterInclude::Im,
2162                SlackConversationFilterInclude::Mpim,
2163            ])
2164            .with_exclude_bot_users(true);
2165
2166        let json = serde_json::to_value(&filter)?;
2167        assert_eq!(
2168            json,
2169            serde_json::json!({
2170                "include": ["im", "mpim"],
2171                "exclude_bot_users": true
2172            })
2173        );
2174        Ok(())
2175    }
2176
2177    #[test]
2178    fn test_conversation_filter_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2179        let elem = SlackBlockConversationsSelectElement::new(SlackActionId("test_action".into()))
2180            .with_filter(
2181                SlackBlockConversationFilter::new()
2182                    .with_include(vec![SlackConversationFilterInclude::Public])
2183                    .with_exclude_external_shared_channels(true),
2184            );
2185
2186        let json = serde_json::to_string(&elem)?;
2187        let parsed: SlackBlockConversationsSelectElement = serde_json::from_str(&json)?;
2188        assert_eq!(elem, parsed);
2189        Ok(())
2190    }
2191
2192    #[test]
2193    fn test_multi_conversations_select_filter() -> Result<(), Box<dyn std::error::Error>> {
2194        let elem =
2195            SlackBlockMultiConversationsSelectElement::new(SlackActionId("multi_action".into()))
2196                .with_filter(
2197                    SlackBlockConversationFilter::new()
2198                        .with_include(vec![
2199                            SlackConversationFilterInclude::Public,
2200                            SlackConversationFilterInclude::Private,
2201                        ])
2202                        .with_exclude_bot_users(true),
2203                );
2204
2205        let json = serde_json::to_string(&elem)?;
2206        let parsed: SlackBlockMultiConversationsSelectElement = serde_json::from_str(&json)?;
2207        assert_eq!(elem, parsed);
2208        Ok(())
2209    }
2210
2211    #[test]
2212    fn test_conversation_filter_none_omitted() -> Result<(), Box<dyn std::error::Error>> {
2213        let elem = SlackBlockConversationsSelectElement::new(SlackActionId("no_filter".into()));
2214
2215        let json = serde_json::to_value(&elem)?;
2216        assert!(json.get("filter").is_none());
2217        Ok(())
2218    }
2219
2220    #[test]
2221    fn test_slack_image_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2222        let payload = include_str!("./fixtures/slack_image_blocks.json");
2223        let content: SlackMessageContent = serde_json::from_str(payload)?;
2224        let blocks = content.blocks.expect("Blocks should not be empty");
2225        match blocks.first() {
2226            Some(SlackBlock::Section(section)) => match &section.accessory {
2227                Some(SlackSectionBlockElement::Image(image)) => {
2228                    assert_eq!(image.alt_text, "alt text for image");
2229                    match &image.image_url_or_file {
2230                        SlackImageUrlOrFile::ImageUrl { image_url } => {
2231                            assert_eq!(image_url.as_str(), "https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg");
2232                        }
2233                        SlackImageUrlOrFile::SlackFile { slack_file } => {
2234                            panic!("Expected an image URL, not a Slack file: {:?}", slack_file);
2235                        }
2236                    }
2237                }
2238                _ => panic!("Expected a section block with an image accessory"),
2239            },
2240            _ => panic!("Expected a section block"),
2241        }
2242        Ok(())
2243    }
2244
2245    #[test]
2246    fn test_rich_text_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2247        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2248        let block: SlackBlock = serde_json::from_str(payload)?;
2249
2250        let rich = match block {
2251            SlackBlock::RichText(r) => r,
2252            _ => panic!("Expected a RichText block"),
2253        };
2254
2255        assert_eq!(rich.block_id, Some(SlackBlockId("test_block".into())));
2256        assert_eq!(rich.elements.len(), 4);
2257
2258        // section
2259        let section = match &rich.elements[0] {
2260            SlackRichTextElement::Section(s) => s,
2261            _ => panic!("Expected a Section element"),
2262        };
2263        assert_eq!(section.elements.len(), 7);
2264
2265        // bold text
2266        let text = match &section.elements[0] {
2267            SlackRichTextInlineElement::Text(t) => t,
2268            _ => panic!("Expected a Text element"),
2269        };
2270        assert_eq!(text.text, "Hello ");
2271        assert_eq!(text.style.as_ref().and_then(|s| s.bold), Some(true));
2272
2273        // user
2274        assert!(matches!(
2275            &section.elements[1],
2276            SlackRichTextInlineElement::User(_)
2277        ));
2278
2279        // emoji — name should deserialize as SlackEmojiName
2280        let emoji = match &section.elements[4] {
2281            SlackRichTextInlineElement::Emoji(e) => e,
2282            _ => panic!("Expected an Emoji element"),
2283        };
2284        assert_eq!(emoji.name, SlackEmojiName::new("wave".into()));
2285
2286        // list
2287        let list = match &rich.elements[1] {
2288            SlackRichTextElement::List(l) => l,
2289            _ => panic!("Expected a List element"),
2290        };
2291        assert_eq!(list.style, SlackRichTextListStyle::Bullet);
2292        assert_eq!(list.elements.len(), 2);
2293
2294        // list items are SlackRichTextElement::Section
2295        assert!(matches!(
2296            &list.elements[0],
2297            SlackRichTextListElement::Section(_)
2298        ));
2299        assert!(matches!(
2300            &list.elements[1],
2301            SlackRichTextListElement::Section(_)
2302        ));
2303
2304        // preformatted
2305        assert!(matches!(
2306            &rich.elements[2],
2307            SlackRichTextElement::Preformatted(_)
2308        ));
2309
2310        // quote
2311        assert!(matches!(&rich.elements[3], SlackRichTextElement::Quote(_)));
2312
2313        Ok(())
2314    }
2315
2316    #[test]
2317    fn test_rich_text_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2318        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2319        let block: SlackBlock = serde_json::from_str(payload)?;
2320        let serialized = serde_json::to_string(&block)?;
2321        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2322        assert_eq!(block, block2);
2323        Ok(())
2324    }
2325
2326    #[test]
2327    fn test_slack_table_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2328        let payload = include_str!("./fixtures/slack_table_block.json");
2329        let block: SlackBlock = serde_json::from_str(payload)?;
2330
2331        let table = match block {
2332            SlackBlock::Table(t) => t,
2333            _ => panic!("Expected a Table block"),
2334        };
2335
2336        assert_eq!(table.block_id, Some(SlackBlockId("table_block_1".into())));
2337        assert_eq!(table.rows.len(), 2);
2338        assert_eq!(table.rows[0].len(), 2);
2339
2340        // first row, first cell is raw_text
2341        match &table.rows[0][0] {
2342            SlackTableCell::RawText(c) => assert_eq!(c.text, "Header A"),
2343            _ => panic!("Expected RawText cell"),
2344        }
2345
2346        // second row, second cell is rich_text
2347        match &table.rows[1][1] {
2348            SlackTableCell::RichText(c) => assert_eq!(c.elements.len(), 1),
2349            _ => panic!("Expected RichText cell"),
2350        }
2351
2352        let settings = table
2353            .column_settings
2354            .expect("column_settings should be present");
2355        assert_eq!(settings.len(), 2);
2356        assert_eq!(settings[0].is_wrapped, Some(true));
2357        assert_eq!(settings[1].align, Some(SlackTableColumnAlign::Right));
2358
2359        Ok(())
2360    }
2361
2362    #[test]
2363    fn test_slack_table_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2364        let payload = include_str!("./fixtures/slack_table_block.json");
2365        let block: SlackBlock = serde_json::from_str(payload)?;
2366        let serialized = serde_json::to_string(&block)?;
2367        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2368        assert_eq!(block, block2);
2369        Ok(())
2370    }
2371
2372    #[test]
2373    fn test_slack_task_card_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2374        let payload = include_str!("./fixtures/slack_task_card_block.json");
2375        let block: SlackBlock = serde_json::from_str(payload)?;
2376
2377        let task_card = match block {
2378            SlackBlock::TaskCard(t) => t,
2379            _ => panic!("Expected a TaskCard block"),
2380        };
2381
2382        assert_eq!(task_card.task_id, SlackTaskId("task_1".into()));
2383        assert_eq!(task_card.title, "Fetching weather data");
2384        assert_eq!(
2385            task_card.block_id,
2386            Some(SlackBlockId("task_card_block_1".into()))
2387        );
2388        assert_eq!(task_card.status, Some(SlackTaskCardStatus::InProgress));
2389
2390        let output = task_card.output.expect("output should be present");
2391        let SlackRichTextInlineContent::RichText(output_block) = output;
2392        assert_eq!(output_block.elements.len(), 1);
2393
2394        let sources = task_card.sources.expect("sources should be present");
2395        assert_eq!(sources.len(), 2);
2396        match &sources[0] {
2397            SlackTaskCardSource::Url(u) => assert_eq!(u.text, "weather.com"),
2398        }
2399
2400        Ok(())
2401    }
2402
2403    #[test]
2404    fn test_slack_task_card_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2405        let payload = include_str!("./fixtures/slack_task_card_block.json");
2406        let block: SlackBlock = serde_json::from_str(payload)?;
2407        let serialized = serde_json::to_string(&block)?;
2408        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2409        assert_eq!(block, block2);
2410        Ok(())
2411    }
2412
2413    #[test]
2414    fn test_slack_task_card_block_serializes_icon_and_hide_title(
2415    ) -> Result<(), Box<dyn std::error::Error>> {
2416        let task_card = SlackTaskCardBlock::new(SlackTaskId("task_1".into()), "Title".into())
2417            .with_icon(SlackTaskCardIcon::new("check".into()))
2418            .with_hide_title(true);
2419        let json = serde_json::to_value(&task_card)?;
2420        assert_eq!(
2421            json["icon"],
2422            serde_json::json!({"type": "icon", "name": "check"})
2423        );
2424        assert_eq!(json["hide_title"], serde_json::json!(true));
2425        Ok(())
2426    }
2427
2428    #[test]
2429    fn test_slack_alert_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2430        let payload = include_str!("./fixtures/slack_alert_block.json");
2431        let block: SlackBlock = serde_json::from_str(payload)?;
2432        match block {
2433            SlackBlock::Alert(alert) => {
2434                assert_eq!(alert.level, Some(SlackAlertLevel::Warning));
2435            }
2436            _ => panic!("Expected Alert block"),
2437        }
2438        Ok(())
2439    }
2440
2441    #[test]
2442    fn test_slack_alert_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2443        let payload = include_str!("./fixtures/slack_alert_block.json");
2444        let block: SlackBlock = serde_json::from_str(payload)?;
2445        let serialized = serde_json::to_string(&block)?;
2446        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2447        assert_eq!(block, block2);
2448        Ok(())
2449    }
2450
2451    #[test]
2452    fn test_slack_card_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2453        let payload = include_str!("./fixtures/slack_card_block.json");
2454        let block: SlackBlock = serde_json::from_str(payload)?;
2455        match block {
2456            SlackBlock::Card(card) => {
2457                assert!(card.hero_image.is_some());
2458                assert!(card.actions.is_some());
2459                let actions = card.actions.unwrap();
2460                assert_eq!(actions.len(), 1);
2461                match &actions[0] {
2462                    SlackCardActionBlockElement::Button(btn) => {
2463                        assert_eq!(btn.style, Some(SlackBlockButtonStyle::Primary));
2464                    }
2465                }
2466            }
2467            _ => panic!("Expected Card block"),
2468        }
2469        Ok(())
2470    }
2471
2472    #[test]
2473    fn test_slack_card_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2474        let payload = include_str!("./fixtures/slack_card_block.json");
2475        let block: SlackBlock = serde_json::from_str(payload)?;
2476        let serialized = serde_json::to_string(&block)?;
2477        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2478        assert_eq!(block, block2);
2479        Ok(())
2480    }
2481
2482    #[test]
2483    fn test_slack_context_actions_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2484        let payload = include_str!("./fixtures/slack_context_actions_block.json");
2485        let block: SlackBlock = serde_json::from_str(payload)?;
2486        match block {
2487            SlackBlock::ContextActions(ctx) => {
2488                assert_eq!(ctx.elements.len(), 1);
2489                match &ctx.elements[0] {
2490                    SlackContextActionBlockElement::IconButton(btn) => {
2491                        assert_eq!(btn.icon, "trash");
2492                    }
2493                    _ => panic!("Expected IconButton element"),
2494                }
2495            }
2496            _ => panic!("Expected ContextActions block"),
2497        }
2498        Ok(())
2499    }
2500
2501    #[test]
2502    fn test_slack_context_actions_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2503        let payload = include_str!("./fixtures/slack_context_actions_block.json");
2504        let block: SlackBlock = serde_json::from_str(payload)?;
2505        let serialized = serde_json::to_string(&block)?;
2506        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2507        assert_eq!(block, block2);
2508        Ok(())
2509    }
2510
2511    #[test]
2512    fn test_slack_workflow_button_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2513        let payload = include_str!("./fixtures/slack_workflow_button.json");
2514        let block: SlackBlock = serde_json::from_str(payload)?;
2515        match block {
2516            SlackBlock::Actions(actions) => {
2517                assert_eq!(actions.elements.len(), 1);
2518                match &actions.elements[0] {
2519                    SlackActionBlockElement::WorkflowButton(btn) => {
2520                        assert_eq!(btn.style, Some(SlackBlockButtonStyle::Primary));
2521                        let params = btn
2522                            .workflow
2523                            .trigger
2524                            .customizable_input_parameters
2525                            .as_ref()
2526                            .expect("params should be present");
2527                        assert_eq!(params.len(), 1);
2528                        assert_eq!(params[0].name, "user_input");
2529                    }
2530                    _ => panic!("Expected WorkflowButton element"),
2531                }
2532            }
2533            _ => panic!("Expected Actions block"),
2534        }
2535        Ok(())
2536    }
2537
2538    #[test]
2539    fn test_slack_workflow_button_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2540        let payload = include_str!("./fixtures/slack_workflow_button.json");
2541        let block: SlackBlock = serde_json::from_str(payload)?;
2542        let serialized = serde_json::to_string(&block)?;
2543        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2544        assert_eq!(block, block2);
2545        Ok(())
2546    }
2547
2548    #[test]
2549    fn test_rich_text_message_mention_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2550        let payload = serde_json::json!({
2551            "type": "rich_text",
2552            "block_id": "msgm1",
2553            "elements": [
2554                {
2555                    "type": "rich_text_section",
2556                    "elements": [
2557                        {
2558                            "type": "message_mention",
2559                            "url": "https://acme.slack.com/archives/C12345678/p1784153496441789?thread_ts=1784153496.441789&cid=C12345678",
2560                            "text": "a message",
2561                            "channel_id": "C12345678",
2562                            "author_id": "U12345678",
2563                            "message_ts": "1784153496.441789",
2564                            "thread_ts": "1784153496.441789"
2565                        }
2566                    ]
2567                }
2568            ]
2569        })
2570        .to_string();
2571        let block: SlackBlock = serde_json::from_str(&payload)?;
2572        match block {
2573            SlackBlock::RichText(rich_text) => match &rich_text.elements[0] {
2574                SlackRichTextElement::Section(section) => match &section.elements[0] {
2575                    SlackRichTextInlineElement::MessageMention(mention) => {
2576                        assert_eq!(mention.channel_id, Some(SlackChannelId("C12345678".into())));
2577                        assert_eq!(mention.author_id, Some(SlackUserId("U12345678".into())));
2578                        assert_eq!(
2579                            mention.message_ts,
2580                            Some(SlackTs("1784153496.441789".into()))
2581                        );
2582                    }
2583                    other => panic!("Expected MessageMention element, got {other:?}"),
2584                },
2585                _ => panic!("Expected Section element"),
2586            },
2587            _ => panic!("Expected RichText block"),
2588        }
2589        Ok(())
2590    }
2591
2592    #[test]
2593    fn test_rich_text_unknown_inline_element_deserialize() -> Result<(), Box<dyn std::error::Error>>
2594    {
2595        let payload = serde_json::json!({
2596            "type": "rich_text_section",
2597            "elements": [
2598                {
2599                    "type": "some_future_element",
2600                    "foo": "bar"
2601                }
2602            ]
2603        })
2604        .to_string();
2605        let section: SlackRichTextElement = serde_json::from_str(&payload)?;
2606        match section {
2607            SlackRichTextElement::Section(section) => match &section.elements[0] {
2608                SlackRichTextInlineElement::Unknown(value) => {
2609                    assert_eq!(value["type"], "some_future_element");
2610                }
2611                other => panic!("Expected Unknown element, got {other:?}"),
2612            },
2613            _ => panic!("Expected Section element"),
2614        }
2615        Ok(())
2616    }
2617
2618    #[test]
2619    fn rich_text_str_converts_to_text_inline_element() -> Result<(), Box<dyn std::error::Error>> {
2620        let element: SlackRichTextInlineElement = "hi".into();
2621        assert_eq!(
2622            serde_json::to_value(&element)?,
2623            serde_json::json!({"type": "text", "text": "hi"})
2624        );
2625
2626        let owned: SlackRichTextInlineElement = "hi".to_string().into();
2627        assert_eq!(
2628            serde_json::to_value(&owned)?,
2629            serde_json::to_value(&element)?
2630        );
2631        Ok(())
2632    }
2633
2634    #[test]
2635    fn rich_text_leaf_elements_convert_to_inline_elements() -> Result<(), Box<dyn std::error::Error>>
2636    {
2637        let text: SlackRichTextInlineElement = SlackRichTextText::new("t".to_string()).into();
2638        assert_eq!(
2639            serde_json::to_value(&text)?,
2640            serde_json::json!({"type": "text", "text": "t"})
2641        );
2642
2643        let link: SlackRichTextInlineElement =
2644            SlackRichTextLink::new(SlackRelaxedUrl("https://example.com".into())).into();
2645        assert_eq!(
2646            serde_json::to_value(&link)?,
2647            serde_json::json!({"type": "link", "url": "https://example.com"})
2648        );
2649
2650        let user: SlackRichTextInlineElement =
2651            SlackRichTextUser::new(SlackUserId("U1".into())).into();
2652        assert_eq!(
2653            serde_json::to_value(&user)?,
2654            serde_json::json!({"type": "user", "user_id": "U1"})
2655        );
2656
2657        let channel: SlackRichTextInlineElement =
2658            SlackRichTextChannel::new(SlackChannelId("C1".into())).into();
2659        assert_eq!(
2660            serde_json::to_value(&channel)?,
2661            serde_json::json!({"type": "channel", "channel_id": "C1"})
2662        );
2663
2664        let usergroup: SlackRichTextInlineElement =
2665            SlackRichTextUserGroup::new(SlackUserGroupId("G1".into())).into();
2666        assert_eq!(
2667            serde_json::to_value(&usergroup)?,
2668            serde_json::json!({"type": "usergroup", "usergroup_id": "G1"})
2669        );
2670
2671        let emoji: SlackRichTextInlineElement =
2672            SlackRichTextEmoji::new(SlackEmojiName("wave".into())).into();
2673        assert_eq!(
2674            serde_json::to_value(&emoji)?,
2675            serde_json::json!({"type": "emoji", "name": "wave"})
2676        );
2677
2678        let date: SlackRichTextInlineElement = SlackRichTextDate::new(
2679            SlackDateTime("2020-01-01T00:42:42Z".parse::<SlackUtcDateTime>()?),
2680            "{date_short}".to_string(),
2681        )
2682        .into();
2683        assert_eq!(
2684            serde_json::to_value(&date)?,
2685            serde_json::json!({"type": "date", "timestamp": 1_577_839_362_i64, "format": "{date_short}"})
2686        );
2687
2688        let broadcast: SlackRichTextInlineElement =
2689            SlackRichTextBroadcast::new(SlackRichTextBroadcastRange::Here).into();
2690        assert_eq!(
2691            serde_json::to_value(&broadcast)?,
2692            serde_json::json!({"type": "broadcast", "range": "here"})
2693        );
2694
2695        let color: SlackRichTextInlineElement =
2696            SlackRichTextColor::new("#ff0000".to_string()).into();
2697        assert_eq!(
2698            serde_json::to_value(&color)?,
2699            serde_json::json!({"type": "color", "value": "#ff0000"})
2700        );
2701
2702        let mention: SlackRichTextInlineElement = SlackRichTextMessageMention::new(
2703            SlackRelaxedUrl("https://example.com/archives/C1/p1".into()),
2704        )
2705        .into();
2706        assert_eq!(
2707            serde_json::to_value(&mention)?,
2708            serde_json::json!({"type": "message_mention", "url": "https://example.com/archives/C1/p1"})
2709        );
2710
2711        Ok(())
2712    }
2713
2714    #[test]
2715    fn rich_text_list_accepts_str_items() -> Result<(), Box<dyn std::error::Error>> {
2716        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2717        let block: SlackBlock = serde_json::from_str(payload)?;
2718        let expected_list = match block {
2719            SlackBlock::RichText(r) => match &r.elements[1] {
2720                SlackRichTextElement::List(l) => l.clone(),
2721                other => panic!("Expected List element, got {other:?}"),
2722            },
2723            _ => panic!("Expected RichText block"),
2724        };
2725
2726        let list = SlackRichTextList::new(
2727            SlackRichTextListStyle::Bullet,
2728            vec!["Item one".into(), "Item two".into()],
2729        )
2730        .with_indent(0);
2731
2732        assert_eq!(list, expected_list);
2733        Ok(())
2734    }
2735
2736    #[test]
2737    fn rich_text_style_helpers_merge_flags() -> Result<(), Box<dyn std::error::Error>> {
2738        let bold = SlackRichTextText::new("hi".to_string()).bold();
2739        let json = serde_json::to_value(&bold)?;
2740        assert_eq!(json["style"], serde_json::json!({"bold": true}));
2741
2742        let both = SlackRichTextText::new("hi".to_string())
2743            .with_style(SlackRichTextStyle::new().with_italic(true))
2744            .bold();
2745        let json2 = serde_json::to_value(&both)?;
2746        assert_eq!(
2747            json2["style"],
2748            serde_json::json!({"bold": true, "italic": true})
2749        );
2750
2751        let struck = SlackRichTextText::new("hi".to_string()).strike();
2752        assert_eq!(
2753            serde_json::to_value(&struck)?["style"],
2754            serde_json::json!({"strike": true})
2755        );
2756
2757        let coded = SlackRichTextText::new("hi".to_string()).code();
2758        assert_eq!(
2759            serde_json::to_value(&coded)?["style"],
2760            serde_json::json!({"code": true})
2761        );
2762
2763        Ok(())
2764    }
2765
2766    #[test]
2767    fn table_block_builds_from_str_cells() -> Result<(), Box<dyn std::error::Error>> {
2768        let payload = include_str!("./fixtures/slack_table_block.json");
2769        let expected: serde_json::Value = serde_json::from_str(payload)?;
2770
2771        let block: SlackBlock = SlackTableBlock::new(vec![
2772            vec!["Header A".into(), "Header B".into()],
2773            vec![
2774                "Data 1A".into(),
2775                SlackTableRichTextCell::new(vec![SlackRichTextSection::new(vec![
2776                    SlackRichTextLink::new(SlackRelaxedUrl("https://slack.com".into()))
2777                        .with_text("Data 1B".to_string())
2778                        .into(),
2779                ])
2780                .into()])
2781                .into(),
2782            ],
2783        ])
2784        .with_block_id(SlackBlockId("table_block_1".into()))
2785        .with_column_settings(vec![
2786            SlackTableColumnSetting::new().with_is_wrapped(true),
2787            SlackTableColumnSetting::new().with_align(SlackTableColumnAlign::Right),
2788        ])
2789        .into();
2790
2791        assert_eq!(serde_json::to_value(&block)?, expected);
2792        Ok(())
2793    }
2794
2795    #[test]
2796    fn rich_text_block_builds_fixture_with_conversions() -> Result<(), Box<dyn std::error::Error>> {
2797        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2798        let expected: serde_json::Value = serde_json::from_str(payload)?;
2799
2800        let section: SlackRichTextElement = SlackRichTextSection::new(vec![
2801            SlackRichTextText::new("Hello ".to_string()).bold().into(),
2802            SlackRichTextUser::new(SlackUserId("U123ABC456".into())).into(),
2803            "! Check out ".into(),
2804            SlackRichTextLink::new(SlackRelaxedUrl("https://example.com".into()))
2805                .with_text("this link".to_string())
2806                .with_style(SlackRichTextStyle::new().with_italic(true))
2807                .into(),
2808            SlackRichTextEmoji::new(SlackEmojiName("wave".into())).into(),
2809            SlackRichTextChannel::new(SlackChannelId("C123ABC456".into())).into(),
2810            SlackRichTextBroadcast::new(SlackRichTextBroadcastRange::Here).into(),
2811        ])
2812        .into();
2813
2814        let list: SlackRichTextElement = SlackRichTextList::new(
2815            SlackRichTextListStyle::Bullet,
2816            vec!["Item one".into(), "Item two".into()],
2817        )
2818        .with_indent(0)
2819        .into();
2820
2821        let preformatted: SlackRichTextElement =
2822            SlackRichTextPreformatted::new(vec![SlackRichTextText::new(
2823                "fn main() {}\n".to_string(),
2824            )
2825            .into()])
2826            .with_border(1)
2827            .into();
2828
2829        let quote: SlackRichTextElement =
2830            SlackRichTextQuote::new(vec![SlackRichTextText::new("A wise quote".to_string())
2831                .italic()
2832                .into()])
2833            .into();
2834
2835        let block: SlackBlock = SlackRichTextBlock::new(vec![section, list, preformatted, quote])
2836            .with_block_id(SlackBlockId("test_block".into()))
2837            .into();
2838
2839        assert_eq!(serde_json::to_value(&block)?, expected);
2840        Ok(())
2841    }
2842
2843    #[test]
2844    fn unknown_block_type_round_trips_verbatim() -> Result<(), Box<dyn std::error::Error>> {
2845        let payload = serde_json::json!([
2846            { "type": "some_future_block", "x": 1 },
2847            { "type": "divider", "block_id": "d1" }
2848        ]);
2849        let blocks: Vec<SlackBlock> = serde_json::from_value(payload.clone())?;
2850        match &blocks[0] {
2851            SlackBlock::Unknown(value) => assert_eq!(value, &payload[0]),
2852            other => panic!("Expected Unknown block, got {other:?}"),
2853        }
2854        assert_eq!(
2855            blocks[1],
2856            SlackBlock::Divider(SlackDividerBlock::new().with_block_id("d1".into()))
2857        );
2858        assert_eq!(serde_json::to_value(&blocks)?, payload);
2859        Ok(())
2860    }
2861
2862    #[test]
2863    fn malformed_known_block_type_falls_back_to_unknown() -> Result<(), Box<dyn std::error::Error>>
2864    {
2865        // `header` requires `text`; serde tries the untagged fallback once the typed body fails.
2866        let payload = serde_json::json!({ "type": "header" });
2867        let block: SlackBlock = serde_json::from_value(payload.clone())?;
2868        assert_eq!(block, SlackBlock::Unknown(payload));
2869        Ok(())
2870    }
2871
2872    /// Deserialises a fixture to `SlackBlock`, re-serialises it and returns the
2873    /// typed block alongside the equality of both JSON values.
2874    fn round_trip(payload: &str) -> Result<SlackBlock, Box<dyn std::error::Error>> {
2875        let expected: serde_json::Value = serde_json::from_str(payload)?;
2876        let block: SlackBlock = serde_json::from_str(payload)?;
2877        assert_eq!(serde_json::to_value(&block)?, expected);
2878        Ok(block)
2879    }
2880
2881    #[test]
2882    fn container_block_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2883        let block = round_trip(include_str!("./fixtures/slack_container_block.json"))?;
2884        let SlackBlock::Container(container) = block else {
2885            panic!("Expected a Container block, got {block:?}");
2886        };
2887        assert_eq!(container.is_collapsible, Some(true));
2888        assert_eq!(container.child_blocks.len(), 6);
2889        assert!(matches!(container.child_blocks[0], SlackBlock::Section(_)));
2890        assert!(matches!(container.child_blocks[5], SlackBlock::Actions(_)));
2891        Ok(())
2892    }
2893
2894    #[test]
2895    fn plan_block_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2896        let block = round_trip(include_str!("./fixtures/slack_plan_block.json"))?;
2897        let SlackBlock::Plan(plan) = block else {
2898            panic!("Expected a Plan block, got {block:?}");
2899        };
2900        assert_eq!(plan.title, "Thinking completed");
2901        assert_eq!(plan.tasks.len(), 3);
2902        assert_eq!(plan.tasks[1].status, Some(SlackTaskCardStatus::Pending));
2903        assert!(plan.tasks[2].output.is_some());
2904        Ok(())
2905    }
2906
2907    #[test]
2908    fn data_table_block_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2909        let block = round_trip(include_str!("./fixtures/slack_data_table_block.json"))?;
2910        let SlackBlock::DataTable(table) = block else {
2911            panic!("Expected a DataTable block, got {block:?}");
2912        };
2913        assert_eq!(table.caption, "A Fabulous Table");
2914        assert_eq!(table.rows.len(), 4);
2915        assert_eq!(table.rows[0][0], "Name".into());
2916        assert!(matches!(table.rows[1][2], SlackDataTableCell::RichText(_)));
2917        Ok(())
2918    }
2919
2920    #[test]
2921    fn data_table_number_and_action_cells_round_trip() -> Result<(), Box<dyn std::error::Error>> {
2922        // `action_cell` example from the docs page plus a `raw_number` and a future cell kind.
2923        let payload = serde_json::json!([
2924            { "type": "raw_number", "value": 1.5, "text": "1.5" },
2925            {
2926                "type": "action_cell",
2927                "element": {
2928                    "type": "button",
2929                    "text": { "type": "plain_text", "text": "Mark done" },
2930                    "action_id": "mark_done",
2931                    "value": "task_123"
2932                },
2933                "fallback": { "type": "raw_text", "text": "Open" }
2934            },
2935            { "type": "sparkline", "points": [1, 2] }
2936        ]);
2937        let cells: Vec<SlackDataTableCell> = serde_json::from_value(payload.clone())?;
2938        assert!(matches!(cells[0], SlackDataTableCell::RawNumber(_)));
2939        let SlackDataTableCell::ActionCell(action) = &cells[1] else {
2940            panic!("Expected an ActionCell, got {:?}", cells[1]);
2941        };
2942        assert_eq!(action.fallback.as_deref(), Some(&"Open".into()));
2943        assert!(matches!(cells[2], SlackDataTableCell::Unknown(_)));
2944        assert_eq!(serde_json::to_value(&cells)?, payload);
2945        Ok(())
2946    }
2947
2948    #[test]
2949    fn data_visualization_pie_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2950        let block = round_trip(include_str!(
2951            "./fixtures/slack_data_visualization_pie_block.json"
2952        ))?;
2953        let SlackBlock::DataVisualization(viz) = block else {
2954            panic!("Expected a DataVisualization block, got {block:?}");
2955        };
2956        let SlackChart::Pie(pie) = viz.chart else {
2957            panic!("Expected a pie chart, got {:?}", viz.chart);
2958        };
2959        assert_eq!(pie.segments.len(), 4);
2960        assert_eq!(pie.segments[0].value, 45.into());
2961        Ok(())
2962    }
2963
2964    #[test]
2965    fn data_visualization_bar_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2966        let block = round_trip(include_str!(
2967            "./fixtures/slack_data_visualization_bar_block.json"
2968        ))?;
2969        let SlackBlock::DataVisualization(viz) = block else {
2970            panic!("Expected a DataVisualization block, got {block:?}");
2971        };
2972        let SlackChart::Bar(bar) = viz.chart else {
2973            panic!("Expected a bar chart, got {:?}", viz.chart);
2974        };
2975        assert_eq!(bar.series[0].data.len(), 5);
2976        assert_eq!(bar.axis_config.categories.len(), 5);
2977        assert_eq!(
2978            bar.axis_config.y_label.as_deref(),
2979            Some("Percentage of Tastiness")
2980        );
2981        Ok(())
2982    }
2983
2984    #[test]
2985    fn stream_chunk_blocks_carries_new_block_types() -> Result<(), Box<dyn std::error::Error>> {
2986        let payload = serde_json::json!({
2987            "type": "blocks",
2988            "blocks": [
2989                { "type": "plan", "title": "Plan", "tasks": [] },
2990                { "type": "some_future_block", "x": 1 }
2991            ]
2992        });
2993        let chunk: crate::api::SlackStreamChunk = serde_json::from_value(payload.clone())?;
2994        let crate::api::SlackStreamChunk::Blocks { blocks } = &chunk else {
2995            panic!("Expected a Blocks chunk, got {chunk:?}");
2996        };
2997        assert!(matches!(blocks[0], SlackBlock::Plan(_)));
2998        assert!(matches!(blocks[1], SlackBlock::Unknown(_)));
2999        assert_eq!(serde_json::to_value(&chunk)?, payload);
3000        Ok(())
3001    }
3002
3003    #[test]
3004    fn header_block_keeps_its_level() -> Result<(), Box<dyn std::error::Error>> {
3005        let payload = serde_json::json!({
3006            "type": "header",
3007            "text": { "type": "plain_text", "text": "Budget", "emoji": true },
3008            "level": 2
3009        });
3010        let block: SlackBlock = serde_json::from_value(payload.clone())?;
3011        let SlackBlock::Header(header) = &block else {
3012            panic!("Expected a header block, got {block:?}");
3013        };
3014        assert_eq!(header.level, Some(SlackHeaderLevel(2)));
3015        assert_eq!(serde_json::to_value(&block)?, payload);
3016        let plain: SlackBlock = SlackHeaderBlock::new(crate::pt!("Budget")).into();
3017        assert!(serde_json::to_value(&plain)?.get("level").is_none());
3018        Ok(())
3019    }
3020}