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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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: Option<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 text: SlackBlockPlainTextOnly,
1834    pub value: String,
1835    pub accessibility_label: Option<SlackAccessibilityLabel>,
1836}
1837
1838#[skip_serializing_none]
1839#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1840pub struct SlackBlockFeedbackButtonsElement {
1841    pub action_id: Option<SlackActionId>,
1842    pub positive_button: SlackFeedbackButtonItem,
1843    pub negative_button: SlackFeedbackButtonItem,
1844}
1845
1846#[skip_serializing_none]
1847#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1848pub struct SlackBlockIconButtonElement {
1849    pub action_id: Option<SlackActionId>,
1850    pub icon: String,
1851    pub text: SlackBlockPlainTextOnly,
1852    pub value: Option<String>,
1853    pub confirm: Option<SlackBlockConfirmItem>,
1854    pub accessibility_label: Option<SlackAccessibilityLabel>,
1855    pub visible_to_user_ids: Option<Vec<SlackUserId>>,
1856}
1857
1858#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1859#[serde(tag = "type")]
1860pub enum SlackContextActionBlockElement {
1861    #[serde(rename = "feedback_buttons")]
1862    FeedbackButtons(SlackBlockFeedbackButtonsElement),
1863    #[serde(rename = "icon_button")]
1864    IconButton(SlackBlockIconButtonElement),
1865}
1866
1867impl From<SlackBlockFeedbackButtonsElement> for SlackContextActionBlockElement {
1868    fn from(element: SlackBlockFeedbackButtonsElement) -> Self {
1869        SlackContextActionBlockElement::FeedbackButtons(element)
1870    }
1871}
1872
1873impl From<SlackBlockIconButtonElement> for SlackContextActionBlockElement {
1874    fn from(element: SlackBlockIconButtonElement) -> Self {
1875        SlackContextActionBlockElement::IconButton(element)
1876    }
1877}
1878
1879#[skip_serializing_none]
1880#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1881pub struct SlackContextActionsBlock {
1882    pub block_id: Option<SlackBlockId>,
1883    pub elements: Vec<SlackContextActionBlockElement>,
1884}
1885
1886impl From<SlackContextActionsBlock> for SlackBlock {
1887    fn from(block: SlackContextActionsBlock) -> Self {
1888        SlackBlock::ContextActions(block)
1889    }
1890}
1891
1892/**
1893 * https://docs.slack.dev/reference/block-kit/blocks/container-block
1894 *
1895 * One of `title` / `rich_text_title` is required; `rich_text_title` wins when
1896 * both are set. `child_blocks` holds at most 10 blocks. The docs list
1897 * `actions, context, divider, file, header, image, input, rich_text, section,
1898 * table, video` as supported children while the live validator accepts
1899 * `divider, image, section, contact_card, callout, canvas_table, actions,
1900 * video, header, context, table, layout`; neither set is enforced here.
1901 */
1902#[skip_serializing_none]
1903#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1904pub struct SlackContainerBlock {
1905    pub block_id: Option<SlackBlockId>,
1906    pub title: Option<SlackBlockPlainTextOnly>,
1907    pub rich_text_title: Option<SlackRichTextInlineContent>,
1908    pub subtitle: Option<SlackBlockText>,
1909    pub child_blocks: Vec<SlackBlock>,
1910    pub width: Option<SlackContainerWidth>,
1911    pub icon: Option<SlackCardImageElement>,
1912    pub is_collapsible: Option<bool>,
1913    pub default_collapsed: Option<bool>,
1914    pub has_header_divider: Option<bool>,
1915}
1916
1917impl From<SlackContainerBlock> for SlackBlock {
1918    fn from(block: SlackContainerBlock) -> Self {
1919        SlackBlock::Container(block)
1920    }
1921}
1922
1923/// `narrow`, `standard` (default) and `wide` are platform-constrained widths;
1924/// `full` fills the available space.
1925#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1926#[serde(rename_all = "snake_case")]
1927pub enum SlackContainerWidth {
1928    Narrow,
1929    Standard,
1930    Wide,
1931    Full,
1932}
1933
1934/**
1935 * https://docs.slack.dev/reference/block-kit/blocks/plan-block
1936 *
1937 * `tasks` holds up to 50 task-card objects without a `type` tag
1938 * (`SlackTaskCardBlock` serialises without one); every `task_id` in a plan
1939 * must be unique.
1940 */
1941#[skip_serializing_none]
1942#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1943pub struct SlackPlanBlock {
1944    pub title: String,
1945    pub tasks: Vec<SlackTaskCardBlock>,
1946    pub block_id: Option<SlackBlockId>,
1947}
1948
1949impl From<SlackPlanBlock> for SlackBlock {
1950    fn from(block: SlackPlanBlock) -> Self {
1951        SlackBlock::Plan(block)
1952    }
1953}
1954
1955/**
1956 * https://docs.slack.dev/reference/block-kit/blocks/data-table-block
1957 *
1958 * The first row is the header and may not hold `rich_text` cells. 2–201 rows,
1959 * 1–20 columns, every row the same length; `page_size` is 1–100 (default 5).
1960 */
1961#[skip_serializing_none]
1962#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
1963pub struct SlackDataTableBlock {
1964    pub caption: String,
1965    pub rows: Vec<Vec<SlackDataTableCell>>,
1966    pub block_id: Option<SlackBlockId>,
1967    pub page_size: Option<u32>,
1968    pub row_header_column_index: Option<u32>,
1969}
1970
1971impl From<SlackDataTableBlock> for SlackBlock {
1972    fn from(block: SlackDataTableBlock) -> Self {
1973        SlackBlock::DataTable(block)
1974    }
1975}
1976
1977#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
1978#[serde(tag = "type")]
1979pub enum SlackDataTableCell {
1980    #[serde(rename = "raw_text")]
1981    RawText(SlackTableRawTextCell),
1982    #[serde(rename = "raw_number")]
1983    RawNumber(SlackDataTableRawNumberCell),
1984    #[serde(rename = "rich_text")]
1985    RichText(SlackTableRichTextCell),
1986    #[serde(rename = "action_cell")]
1987    ActionCell(SlackDataTableActionCell),
1988    #[serde(untagged)]
1989    Unknown(serde_json::Value),
1990}
1991
1992/// A bare string becomes a raw-text cell.
1993impl From<&str> for SlackDataTableCell {
1994    fn from(value: &str) -> Self {
1995        SlackDataTableCell::RawText(SlackTableRawTextCell::new(value.to_string()))
1996    }
1997}
1998
1999impl From<String> for SlackDataTableCell {
2000    fn from(value: String) -> Self {
2001        SlackDataTableCell::RawText(SlackTableRawTextCell::new(value))
2002    }
2003}
2004
2005impl From<SlackTableRawTextCell> for SlackDataTableCell {
2006    fn from(cell: SlackTableRawTextCell) -> Self {
2007        SlackDataTableCell::RawText(cell)
2008    }
2009}
2010
2011impl From<SlackDataTableRawNumberCell> for SlackDataTableCell {
2012    fn from(cell: SlackDataTableRawNumberCell) -> Self {
2013        SlackDataTableCell::RawNumber(cell)
2014    }
2015}
2016
2017impl From<SlackTableRichTextCell> for SlackDataTableCell {
2018    fn from(cell: SlackTableRichTextCell) -> Self {
2019        SlackDataTableCell::RichText(cell)
2020    }
2021}
2022
2023impl From<SlackDataTableActionCell> for SlackDataTableCell {
2024    fn from(cell: SlackDataTableActionCell) -> Self {
2025        SlackDataTableCell::ActionCell(cell)
2026    }
2027}
2028
2029/// `value` sorts the column numerically when every cell in it is a
2030/// `raw_number`; `text` is what is displayed.
2031#[skip_serializing_none]
2032#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2033pub struct SlackDataTableRawNumberCell {
2034    pub value: serde_json::Number,
2035    pub text: String,
2036}
2037
2038/// A button in a cell; `fallback` is a `raw_text` or `raw_number` cell shown
2039/// by clients that do not support action cells.
2040#[skip_serializing_none]
2041#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2042pub struct SlackDataTableActionCell {
2043    pub element: SlackDataTableActionElement,
2044    pub fallback: Option<Box<SlackDataTableCell>>,
2045}
2046
2047#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
2048#[serde(tag = "type")]
2049pub enum SlackDataTableActionElement {
2050    #[serde(rename = "button")]
2051    Button(SlackBlockButtonElement),
2052}
2053
2054impl From<SlackBlockButtonElement> for SlackDataTableActionElement {
2055    fn from(element: SlackBlockButtonElement) -> Self {
2056        SlackDataTableActionElement::Button(element)
2057    }
2058}
2059
2060/**
2061 * https://docs.slack.dev/reference/block-kit/blocks/data-visualization-block
2062 *
2063 * `title` is at most 50 characters; a message may hold at most two of these.
2064 */
2065#[skip_serializing_none]
2066#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2067pub struct SlackDataVisualizationBlock {
2068    pub title: String,
2069    pub chart: SlackChart,
2070    pub block_id: Option<SlackBlockId>,
2071}
2072
2073impl From<SlackDataVisualizationBlock> for SlackBlock {
2074    fn from(block: SlackDataVisualizationBlock) -> Self {
2075        SlackBlock::DataVisualization(block)
2076    }
2077}
2078
2079#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
2080#[serde(tag = "type", rename_all = "snake_case")]
2081pub enum SlackChart {
2082    Pie(SlackPieChart),
2083    Bar(SlackSeriesChart),
2084    Area(SlackSeriesChart),
2085    Line(SlackSeriesChart),
2086}
2087
2088/// 1–12 segments; each renders as `value / sum(values)`.
2089#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2090pub struct SlackPieChart {
2091    pub segments: Vec<SlackChartSegment>,
2092}
2093
2094/// Shared by `bar`, `area` and `line`: 1–12 series, each with exactly one data
2095/// point per entry of `axis_config.categories`, and unique series names.
2096#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2097pub struct SlackSeriesChart {
2098    pub series: Vec<SlackChartDataSeries>,
2099    pub axis_config: SlackChartAxisConfig,
2100}
2101
2102#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2103pub struct SlackChartSegment {
2104    pub label: String,
2105    pub value: serde_json::Number,
2106}
2107
2108#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2109pub struct SlackChartDataSeries {
2110    pub name: String,
2111    pub data: Vec<SlackChartDataPoint>,
2112}
2113
2114#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2115pub struct SlackChartDataPoint {
2116    pub label: String,
2117    pub value: serde_json::Number,
2118}
2119
2120/// `categories` defines the valid data-point labels and their x-axis order.
2121#[skip_serializing_none]
2122#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Builder)]
2123pub struct SlackChartAxisConfig {
2124    pub categories: Vec<String>,
2125    pub x_label: Option<String>,
2126    pub y_label: Option<String>,
2127}
2128
2129#[cfg(test)]
2130mod test {
2131    use super::*;
2132    use crate::blocks::SlackHomeView;
2133
2134    #[test]
2135    fn test_conversation_filter_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2136        let payload = include_str!("./fixtures/slack_conversations_select_with_filter.json");
2137        let block: SlackBlock = serde_json::from_str(payload)?;
2138        match block {
2139            SlackBlock::Section(section) => match section.accessory {
2140                Some(SlackSectionBlockElement::ConversationsSelect(elem)) => {
2141                    let filter = elem.filter.expect("filter should be present");
2142                    let include = filter.include.expect("include should be present");
2143                    assert_eq!(include.len(), 2);
2144                    assert_eq!(include[0], SlackConversationFilterInclude::Public);
2145                    assert_eq!(include[1], SlackConversationFilterInclude::Private);
2146                    assert_eq!(filter.exclude_external_shared_channels, Some(true));
2147                    assert_eq!(filter.exclude_bot_users, Some(true));
2148                }
2149                _ => panic!("Expected ConversationsSelect accessory"),
2150            },
2151            _ => panic!("Expected Section block"),
2152        }
2153        Ok(())
2154    }
2155
2156    #[test]
2157    fn test_conversation_filter_serialize() -> Result<(), Box<dyn std::error::Error>> {
2158        let filter = SlackBlockConversationFilter::new()
2159            .with_include(vec![
2160                SlackConversationFilterInclude::Im,
2161                SlackConversationFilterInclude::Mpim,
2162            ])
2163            .with_exclude_bot_users(true);
2164
2165        let json = serde_json::to_value(&filter)?;
2166        assert_eq!(
2167            json,
2168            serde_json::json!({
2169                "include": ["im", "mpim"],
2170                "exclude_bot_users": true
2171            })
2172        );
2173        Ok(())
2174    }
2175
2176    #[test]
2177    fn test_conversation_filter_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2178        let elem = SlackBlockConversationsSelectElement::new()
2179            .with_action_id(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 = SlackBlockMultiConversationsSelectElement::new()
2195            .with_action_id(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()
2214            .with_action_id(SlackActionId("no_filter".into()));
2215
2216        let json = serde_json::to_value(&elem)?;
2217        assert!(json.get("filter").is_none());
2218        Ok(())
2219    }
2220
2221    #[test]
2222    fn test_slack_image_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2223        let payload = include_str!("./fixtures/slack_image_blocks.json");
2224        let content: SlackMessageContent = serde_json::from_str(payload)?;
2225        let blocks = content.blocks.expect("Blocks should not be empty");
2226        match blocks.first() {
2227            Some(SlackBlock::Section(section)) => match &section.accessory {
2228                Some(SlackSectionBlockElement::Image(image)) => {
2229                    assert_eq!(image.alt_text, "alt text for image");
2230                    match &image.image_url_or_file {
2231                        SlackImageUrlOrFile::ImageUrl { image_url } => {
2232                            assert_eq!(image_url.as_str(), "https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg");
2233                        }
2234                        SlackImageUrlOrFile::SlackFile { slack_file } => {
2235                            panic!("Expected an image URL, not a Slack file: {:?}", slack_file);
2236                        }
2237                    }
2238                }
2239                _ => panic!("Expected a section block with an image accessory"),
2240            },
2241            _ => panic!("Expected a section block"),
2242        }
2243        Ok(())
2244    }
2245
2246    #[test]
2247    fn test_rich_text_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2248        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2249        let block: SlackBlock = serde_json::from_str(payload)?;
2250
2251        let rich = match block {
2252            SlackBlock::RichText(r) => r,
2253            _ => panic!("Expected a RichText block"),
2254        };
2255
2256        assert_eq!(rich.block_id, Some(SlackBlockId("test_block".into())));
2257        assert_eq!(rich.elements.len(), 4);
2258
2259        // section
2260        let section = match &rich.elements[0] {
2261            SlackRichTextElement::Section(s) => s,
2262            _ => panic!("Expected a Section element"),
2263        };
2264        assert_eq!(section.elements.len(), 7);
2265
2266        // bold text
2267        let text = match &section.elements[0] {
2268            SlackRichTextInlineElement::Text(t) => t,
2269            _ => panic!("Expected a Text element"),
2270        };
2271        assert_eq!(text.text, "Hello ");
2272        assert_eq!(text.style.as_ref().and_then(|s| s.bold), Some(true));
2273
2274        // user
2275        assert!(matches!(
2276            &section.elements[1],
2277            SlackRichTextInlineElement::User(_)
2278        ));
2279
2280        // emoji — name should deserialize as SlackEmojiName
2281        let emoji = match &section.elements[4] {
2282            SlackRichTextInlineElement::Emoji(e) => e,
2283            _ => panic!("Expected an Emoji element"),
2284        };
2285        assert_eq!(emoji.name, SlackEmojiName::new("wave".into()));
2286
2287        // list
2288        let list = match &rich.elements[1] {
2289            SlackRichTextElement::List(l) => l,
2290            _ => panic!("Expected a List element"),
2291        };
2292        assert_eq!(list.style, SlackRichTextListStyle::Bullet);
2293        assert_eq!(list.elements.len(), 2);
2294
2295        // list items are SlackRichTextElement::Section
2296        assert!(matches!(
2297            &list.elements[0],
2298            SlackRichTextListElement::Section(_)
2299        ));
2300        assert!(matches!(
2301            &list.elements[1],
2302            SlackRichTextListElement::Section(_)
2303        ));
2304
2305        // preformatted
2306        assert!(matches!(
2307            &rich.elements[2],
2308            SlackRichTextElement::Preformatted(_)
2309        ));
2310
2311        // quote
2312        assert!(matches!(&rich.elements[3], SlackRichTextElement::Quote(_)));
2313
2314        Ok(())
2315    }
2316
2317    #[test]
2318    fn test_rich_text_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2319        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2320        let block: SlackBlock = serde_json::from_str(payload)?;
2321        let serialized = serde_json::to_string(&block)?;
2322        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2323        assert_eq!(block, block2);
2324        Ok(())
2325    }
2326
2327    #[test]
2328    fn test_slack_table_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2329        let payload = include_str!("./fixtures/slack_table_block.json");
2330        let block: SlackBlock = serde_json::from_str(payload)?;
2331
2332        let table = match block {
2333            SlackBlock::Table(t) => t,
2334            _ => panic!("Expected a Table block"),
2335        };
2336
2337        assert_eq!(table.block_id, Some(SlackBlockId("table_block_1".into())));
2338        assert_eq!(table.rows.len(), 2);
2339        assert_eq!(table.rows[0].len(), 2);
2340
2341        // first row, first cell is raw_text
2342        match &table.rows[0][0] {
2343            SlackTableCell::RawText(c) => assert_eq!(c.text, "Header A"),
2344            _ => panic!("Expected RawText cell"),
2345        }
2346
2347        // second row, second cell is rich_text
2348        match &table.rows[1][1] {
2349            SlackTableCell::RichText(c) => assert_eq!(c.elements.len(), 1),
2350            _ => panic!("Expected RichText cell"),
2351        }
2352
2353        let settings = table
2354            .column_settings
2355            .expect("column_settings should be present");
2356        assert_eq!(settings.len(), 2);
2357        assert_eq!(settings[0].is_wrapped, Some(true));
2358        assert_eq!(settings[1].align, Some(SlackTableColumnAlign::Right));
2359
2360        Ok(())
2361    }
2362
2363    #[test]
2364    fn test_slack_table_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2365        let payload = include_str!("./fixtures/slack_table_block.json");
2366        let block: SlackBlock = serde_json::from_str(payload)?;
2367        let serialized = serde_json::to_string(&block)?;
2368        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2369        assert_eq!(block, block2);
2370        Ok(())
2371    }
2372
2373    #[test]
2374    fn test_slack_task_card_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2375        let payload = include_str!("./fixtures/slack_task_card_block.json");
2376        let block: SlackBlock = serde_json::from_str(payload)?;
2377
2378        let task_card = match block {
2379            SlackBlock::TaskCard(t) => t,
2380            _ => panic!("Expected a TaskCard block"),
2381        };
2382
2383        assert_eq!(task_card.task_id, SlackTaskId("task_1".into()));
2384        assert_eq!(task_card.title, "Fetching weather data");
2385        assert_eq!(
2386            task_card.block_id,
2387            Some(SlackBlockId("task_card_block_1".into()))
2388        );
2389        assert_eq!(task_card.status, Some(SlackTaskCardStatus::InProgress));
2390
2391        let output = task_card.output.expect("output should be present");
2392        let SlackRichTextInlineContent::RichText(output_block) = output;
2393        assert_eq!(output_block.elements.len(), 1);
2394
2395        let sources = task_card.sources.expect("sources should be present");
2396        assert_eq!(sources.len(), 2);
2397        match &sources[0] {
2398            SlackTaskCardSource::Url(u) => assert_eq!(u.text, "weather.com"),
2399        }
2400
2401        Ok(())
2402    }
2403
2404    #[test]
2405    fn test_slack_task_card_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2406        let payload = include_str!("./fixtures/slack_task_card_block.json");
2407        let block: SlackBlock = serde_json::from_str(payload)?;
2408        let serialized = serde_json::to_string(&block)?;
2409        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2410        assert_eq!(block, block2);
2411        Ok(())
2412    }
2413
2414    #[test]
2415    fn test_slack_task_card_block_serializes_icon_and_hide_title(
2416    ) -> Result<(), Box<dyn std::error::Error>> {
2417        let task_card = SlackTaskCardBlock::new(SlackTaskId("task_1".into()), "Title".into())
2418            .with_icon(SlackTaskCardIcon::new("check".into()))
2419            .with_hide_title(true);
2420        let json = serde_json::to_value(&task_card)?;
2421        assert_eq!(
2422            json["icon"],
2423            serde_json::json!({"type": "icon", "name": "check"})
2424        );
2425        assert_eq!(json["hide_title"], serde_json::json!(true));
2426        Ok(())
2427    }
2428
2429    #[test]
2430    fn test_slack_alert_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2431        let payload = include_str!("./fixtures/slack_alert_block.json");
2432        let block: SlackBlock = serde_json::from_str(payload)?;
2433        match block {
2434            SlackBlock::Alert(alert) => {
2435                assert_eq!(alert.level, Some(SlackAlertLevel::Warning));
2436            }
2437            _ => panic!("Expected Alert block"),
2438        }
2439        Ok(())
2440    }
2441
2442    #[test]
2443    fn test_slack_alert_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2444        let payload = include_str!("./fixtures/slack_alert_block.json");
2445        let block: SlackBlock = serde_json::from_str(payload)?;
2446        let serialized = serde_json::to_string(&block)?;
2447        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2448        assert_eq!(block, block2);
2449        Ok(())
2450    }
2451
2452    #[test]
2453    fn test_slack_card_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2454        let payload = include_str!("./fixtures/slack_card_block.json");
2455        let block: SlackBlock = serde_json::from_str(payload)?;
2456        match block {
2457            SlackBlock::Card(card) => {
2458                assert!(card.hero_image.is_some());
2459                assert!(card.actions.is_some());
2460                let actions = card.actions.unwrap();
2461                assert_eq!(actions.len(), 1);
2462                match &actions[0] {
2463                    SlackCardActionBlockElement::Button(btn) => {
2464                        assert_eq!(btn.style, Some(SlackBlockButtonStyle::Primary));
2465                    }
2466                }
2467            }
2468            _ => panic!("Expected Card block"),
2469        }
2470        Ok(())
2471    }
2472
2473    #[test]
2474    fn test_slack_card_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2475        let payload = include_str!("./fixtures/slack_card_block.json");
2476        let block: SlackBlock = serde_json::from_str(payload)?;
2477        let serialized = serde_json::to_string(&block)?;
2478        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2479        assert_eq!(block, block2);
2480        Ok(())
2481    }
2482
2483    #[test]
2484    fn test_slack_context_actions_block_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2485        let payload = include_str!("./fixtures/slack_context_actions_block.json");
2486        let block: SlackBlock = serde_json::from_str(payload)?;
2487        match block {
2488            SlackBlock::ContextActions(ctx) => {
2489                assert_eq!(ctx.elements.len(), 1);
2490                match &ctx.elements[0] {
2491                    SlackContextActionBlockElement::IconButton(btn) => {
2492                        assert_eq!(btn.icon, "trash");
2493                    }
2494                    _ => panic!("Expected IconButton element"),
2495                }
2496            }
2497            _ => panic!("Expected ContextActions block"),
2498        }
2499        Ok(())
2500    }
2501
2502    #[test]
2503    fn test_slack_context_actions_block_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2504        let payload = include_str!("./fixtures/slack_context_actions_block.json");
2505        let block: SlackBlock = serde_json::from_str(payload)?;
2506        let serialized = serde_json::to_string(&block)?;
2507        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2508        assert_eq!(block, block2);
2509        Ok(())
2510    }
2511
2512    #[test]
2513    fn test_slack_workflow_button_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2514        let payload = include_str!("./fixtures/slack_workflow_button.json");
2515        let block: SlackBlock = serde_json::from_str(payload)?;
2516        match block {
2517            SlackBlock::Actions(actions) => {
2518                assert_eq!(actions.elements.len(), 1);
2519                match &actions.elements[0] {
2520                    SlackActionBlockElement::WorkflowButton(btn) => {
2521                        assert_eq!(btn.style, Some(SlackBlockButtonStyle::Primary));
2522                        let params = btn
2523                            .workflow
2524                            .trigger
2525                            .customizable_input_parameters
2526                            .as_ref()
2527                            .expect("params should be present");
2528                        assert_eq!(params.len(), 1);
2529                        assert_eq!(params[0].name, "user_input");
2530                    }
2531                    _ => panic!("Expected WorkflowButton element"),
2532                }
2533            }
2534            _ => panic!("Expected Actions block"),
2535        }
2536        Ok(())
2537    }
2538
2539    #[test]
2540    fn test_slack_workflow_button_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
2541        let payload = include_str!("./fixtures/slack_workflow_button.json");
2542        let block: SlackBlock = serde_json::from_str(payload)?;
2543        let serialized = serde_json::to_string(&block)?;
2544        let block2: SlackBlock = serde_json::from_str(&serialized)?;
2545        assert_eq!(block, block2);
2546        Ok(())
2547    }
2548
2549    #[test]
2550    fn test_rich_text_message_mention_deserialize() -> Result<(), Box<dyn std::error::Error>> {
2551        let payload = serde_json::json!({
2552            "type": "rich_text",
2553            "block_id": "msgm1",
2554            "elements": [
2555                {
2556                    "type": "rich_text_section",
2557                    "elements": [
2558                        {
2559                            "type": "message_mention",
2560                            "url": "https://acme.slack.com/archives/C12345678/p1784153496441789?thread_ts=1784153496.441789&cid=C12345678",
2561                            "text": "a message",
2562                            "channel_id": "C12345678",
2563                            "author_id": "U12345678",
2564                            "message_ts": "1784153496.441789",
2565                            "thread_ts": "1784153496.441789"
2566                        }
2567                    ]
2568                }
2569            ]
2570        })
2571        .to_string();
2572        let block: SlackBlock = serde_json::from_str(&payload)?;
2573        match block {
2574            SlackBlock::RichText(rich_text) => match &rich_text.elements[0] {
2575                SlackRichTextElement::Section(section) => match &section.elements[0] {
2576                    SlackRichTextInlineElement::MessageMention(mention) => {
2577                        assert_eq!(mention.channel_id, Some(SlackChannelId("C12345678".into())));
2578                        assert_eq!(mention.author_id, Some(SlackUserId("U12345678".into())));
2579                        assert_eq!(
2580                            mention.message_ts,
2581                            Some(SlackTs("1784153496.441789".into()))
2582                        );
2583                    }
2584                    other => panic!("Expected MessageMention element, got {other:?}"),
2585                },
2586                _ => panic!("Expected Section element"),
2587            },
2588            _ => panic!("Expected RichText block"),
2589        }
2590        Ok(())
2591    }
2592
2593    #[test]
2594    fn test_rich_text_unknown_inline_element_deserialize() -> Result<(), Box<dyn std::error::Error>>
2595    {
2596        let payload = serde_json::json!({
2597            "type": "rich_text_section",
2598            "elements": [
2599                {
2600                    "type": "some_future_element",
2601                    "foo": "bar"
2602                }
2603            ]
2604        })
2605        .to_string();
2606        let section: SlackRichTextElement = serde_json::from_str(&payload)?;
2607        match section {
2608            SlackRichTextElement::Section(section) => match &section.elements[0] {
2609                SlackRichTextInlineElement::Unknown(value) => {
2610                    assert_eq!(value["type"], "some_future_element");
2611                }
2612                other => panic!("Expected Unknown element, got {other:?}"),
2613            },
2614            _ => panic!("Expected Section element"),
2615        }
2616        Ok(())
2617    }
2618
2619    #[test]
2620    fn rich_text_str_converts_to_text_inline_element() -> Result<(), Box<dyn std::error::Error>> {
2621        let element: SlackRichTextInlineElement = "hi".into();
2622        assert_eq!(
2623            serde_json::to_value(&element)?,
2624            serde_json::json!({"type": "text", "text": "hi"})
2625        );
2626
2627        let owned: SlackRichTextInlineElement = "hi".to_string().into();
2628        assert_eq!(
2629            serde_json::to_value(&owned)?,
2630            serde_json::to_value(&element)?
2631        );
2632        Ok(())
2633    }
2634
2635    #[test]
2636    fn rich_text_leaf_elements_convert_to_inline_elements() -> Result<(), Box<dyn std::error::Error>>
2637    {
2638        let text: SlackRichTextInlineElement = SlackRichTextText::new("t".to_string()).into();
2639        assert_eq!(
2640            serde_json::to_value(&text)?,
2641            serde_json::json!({"type": "text", "text": "t"})
2642        );
2643
2644        let link: SlackRichTextInlineElement =
2645            SlackRichTextLink::new(SlackRelaxedUrl("https://example.com".into())).into();
2646        assert_eq!(
2647            serde_json::to_value(&link)?,
2648            serde_json::json!({"type": "link", "url": "https://example.com"})
2649        );
2650
2651        let user: SlackRichTextInlineElement =
2652            SlackRichTextUser::new(SlackUserId("U1".into())).into();
2653        assert_eq!(
2654            serde_json::to_value(&user)?,
2655            serde_json::json!({"type": "user", "user_id": "U1"})
2656        );
2657
2658        let channel: SlackRichTextInlineElement =
2659            SlackRichTextChannel::new(SlackChannelId("C1".into())).into();
2660        assert_eq!(
2661            serde_json::to_value(&channel)?,
2662            serde_json::json!({"type": "channel", "channel_id": "C1"})
2663        );
2664
2665        let usergroup: SlackRichTextInlineElement =
2666            SlackRichTextUserGroup::new(SlackUserGroupId("G1".into())).into();
2667        assert_eq!(
2668            serde_json::to_value(&usergroup)?,
2669            serde_json::json!({"type": "usergroup", "usergroup_id": "G1"})
2670        );
2671
2672        let emoji: SlackRichTextInlineElement =
2673            SlackRichTextEmoji::new(SlackEmojiName("wave".into())).into();
2674        assert_eq!(
2675            serde_json::to_value(&emoji)?,
2676            serde_json::json!({"type": "emoji", "name": "wave"})
2677        );
2678
2679        let date: SlackRichTextInlineElement = SlackRichTextDate::new(
2680            SlackDateTime("2020-01-01T00:42:42Z".parse::<SlackUtcDateTime>()?),
2681            "{date_short}".to_string(),
2682        )
2683        .into();
2684        assert_eq!(
2685            serde_json::to_value(&date)?,
2686            serde_json::json!({"type": "date", "timestamp": 1_577_839_362_i64, "format": "{date_short}"})
2687        );
2688
2689        let broadcast: SlackRichTextInlineElement =
2690            SlackRichTextBroadcast::new(SlackRichTextBroadcastRange::Here).into();
2691        assert_eq!(
2692            serde_json::to_value(&broadcast)?,
2693            serde_json::json!({"type": "broadcast", "range": "here"})
2694        );
2695
2696        let color: SlackRichTextInlineElement =
2697            SlackRichTextColor::new("#ff0000".to_string()).into();
2698        assert_eq!(
2699            serde_json::to_value(&color)?,
2700            serde_json::json!({"type": "color", "value": "#ff0000"})
2701        );
2702
2703        let mention: SlackRichTextInlineElement = SlackRichTextMessageMention::new(
2704            SlackRelaxedUrl("https://example.com/archives/C1/p1".into()),
2705        )
2706        .into();
2707        assert_eq!(
2708            serde_json::to_value(&mention)?,
2709            serde_json::json!({"type": "message_mention", "url": "https://example.com/archives/C1/p1"})
2710        );
2711
2712        Ok(())
2713    }
2714
2715    #[test]
2716    fn rich_text_list_accepts_str_items() -> Result<(), Box<dyn std::error::Error>> {
2717        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2718        let block: SlackBlock = serde_json::from_str(payload)?;
2719        let expected_list = match block {
2720            SlackBlock::RichText(r) => match &r.elements[1] {
2721                SlackRichTextElement::List(l) => l.clone(),
2722                other => panic!("Expected List element, got {other:?}"),
2723            },
2724            _ => panic!("Expected RichText block"),
2725        };
2726
2727        let list = SlackRichTextList::new(
2728            SlackRichTextListStyle::Bullet,
2729            vec!["Item one".into(), "Item two".into()],
2730        )
2731        .with_indent(0);
2732
2733        assert_eq!(list, expected_list);
2734        Ok(())
2735    }
2736
2737    #[test]
2738    fn rich_text_style_helpers_merge_flags() -> Result<(), Box<dyn std::error::Error>> {
2739        let bold = SlackRichTextText::new("hi".to_string()).bold();
2740        let json = serde_json::to_value(&bold)?;
2741        assert_eq!(json["style"], serde_json::json!({"bold": true}));
2742
2743        let both = SlackRichTextText::new("hi".to_string())
2744            .with_style(SlackRichTextStyle::new().with_italic(true))
2745            .bold();
2746        let json2 = serde_json::to_value(&both)?;
2747        assert_eq!(
2748            json2["style"],
2749            serde_json::json!({"bold": true, "italic": true})
2750        );
2751
2752        let struck = SlackRichTextText::new("hi".to_string()).strike();
2753        assert_eq!(
2754            serde_json::to_value(&struck)?["style"],
2755            serde_json::json!({"strike": true})
2756        );
2757
2758        let coded = SlackRichTextText::new("hi".to_string()).code();
2759        assert_eq!(
2760            serde_json::to_value(&coded)?["style"],
2761            serde_json::json!({"code": true})
2762        );
2763
2764        Ok(())
2765    }
2766
2767    #[test]
2768    fn table_block_builds_from_str_cells() -> Result<(), Box<dyn std::error::Error>> {
2769        let payload = include_str!("./fixtures/slack_table_block.json");
2770        let expected: serde_json::Value = serde_json::from_str(payload)?;
2771
2772        let block: SlackBlock = SlackTableBlock::new(vec![
2773            vec!["Header A".into(), "Header B".into()],
2774            vec![
2775                "Data 1A".into(),
2776                SlackTableRichTextCell::new(vec![SlackRichTextSection::new(vec![
2777                    SlackRichTextLink::new(SlackRelaxedUrl("https://slack.com".into()))
2778                        .with_text("Data 1B".to_string())
2779                        .into(),
2780                ])
2781                .into()])
2782                .into(),
2783            ],
2784        ])
2785        .with_block_id(SlackBlockId("table_block_1".into()))
2786        .with_column_settings(vec![
2787            SlackTableColumnSetting::new().with_is_wrapped(true),
2788            SlackTableColumnSetting::new().with_align(SlackTableColumnAlign::Right),
2789        ])
2790        .into();
2791
2792        assert_eq!(serde_json::to_value(&block)?, expected);
2793        Ok(())
2794    }
2795
2796    #[test]
2797    fn rich_text_block_builds_fixture_with_conversions() -> Result<(), Box<dyn std::error::Error>> {
2798        let payload = include_str!("./fixtures/slack_rich_text_block.json");
2799        let expected: serde_json::Value = serde_json::from_str(payload)?;
2800
2801        let section: SlackRichTextElement = SlackRichTextSection::new(vec![
2802            SlackRichTextText::new("Hello ".to_string()).bold().into(),
2803            SlackRichTextUser::new(SlackUserId("U123ABC456".into())).into(),
2804            "! Check out ".into(),
2805            SlackRichTextLink::new(SlackRelaxedUrl("https://example.com".into()))
2806                .with_text("this link".to_string())
2807                .with_style(SlackRichTextStyle::new().with_italic(true))
2808                .into(),
2809            SlackRichTextEmoji::new(SlackEmojiName("wave".into())).into(),
2810            SlackRichTextChannel::new(SlackChannelId("C123ABC456".into())).into(),
2811            SlackRichTextBroadcast::new(SlackRichTextBroadcastRange::Here).into(),
2812        ])
2813        .into();
2814
2815        let list: SlackRichTextElement = SlackRichTextList::new(
2816            SlackRichTextListStyle::Bullet,
2817            vec!["Item one".into(), "Item two".into()],
2818        )
2819        .with_indent(0)
2820        .into();
2821
2822        let preformatted: SlackRichTextElement =
2823            SlackRichTextPreformatted::new(vec![SlackRichTextText::new(
2824                "fn main() {}\n".to_string(),
2825            )
2826            .into()])
2827            .with_border(1)
2828            .into();
2829
2830        let quote: SlackRichTextElement =
2831            SlackRichTextQuote::new(vec![SlackRichTextText::new("A wise quote".to_string())
2832                .italic()
2833                .into()])
2834            .into();
2835
2836        let block: SlackBlock = SlackRichTextBlock::new(vec![section, list, preformatted, quote])
2837            .with_block_id(SlackBlockId("test_block".into()))
2838            .into();
2839
2840        assert_eq!(serde_json::to_value(&block)?, expected);
2841        Ok(())
2842    }
2843
2844    #[test]
2845    fn unknown_block_type_round_trips_verbatim() -> Result<(), Box<dyn std::error::Error>> {
2846        let payload = serde_json::json!([
2847            { "type": "some_future_block", "x": 1 },
2848            { "type": "divider", "block_id": "d1" }
2849        ]);
2850        let blocks: Vec<SlackBlock> = serde_json::from_value(payload.clone())?;
2851        match &blocks[0] {
2852            SlackBlock::Unknown(value) => assert_eq!(value, &payload[0]),
2853            other => panic!("Expected Unknown block, got {other:?}"),
2854        }
2855        assert_eq!(
2856            blocks[1],
2857            SlackBlock::Divider(SlackDividerBlock::new().with_block_id("d1".into()))
2858        );
2859        assert_eq!(serde_json::to_value(&blocks)?, payload);
2860        Ok(())
2861    }
2862
2863    #[test]
2864    fn malformed_known_block_type_falls_back_to_unknown() -> Result<(), Box<dyn std::error::Error>>
2865    {
2866        // `header` requires `text`; serde tries the untagged fallback once the typed body fails.
2867        let payload = serde_json::json!({ "type": "header" });
2868        let block: SlackBlock = serde_json::from_value(payload.clone())?;
2869        assert_eq!(block, SlackBlock::Unknown(payload));
2870        Ok(())
2871    }
2872
2873    /// Deserialises a fixture to `SlackBlock`, re-serialises it and returns the
2874    /// typed block alongside the equality of both JSON values.
2875    fn round_trip(payload: &str) -> Result<SlackBlock, Box<dyn std::error::Error>> {
2876        let expected: serde_json::Value = serde_json::from_str(payload)?;
2877        let block: SlackBlock = serde_json::from_str(payload)?;
2878        assert_eq!(serde_json::to_value(&block)?, expected);
2879        Ok(block)
2880    }
2881
2882    #[test]
2883    fn buttons_without_action_id_stay_a_typed_actions_block(
2884    ) -> Result<(), Box<dyn std::error::Error>> {
2885        let block = round_trip(include_str!(
2886            "./fixtures/slack_actions_block_without_action_id.json"
2887        ))?;
2888        let SlackBlock::Actions(actions) = block else {
2889            panic!("Expected an Actions block, got {block:?}");
2890        };
2891        assert_eq!(actions.elements.len(), 2);
2892        assert!(actions
2893            .elements
2894            .iter()
2895            .all(|element| matches!(element, SlackActionBlockElement::Button(_))));
2896        Ok(())
2897    }
2898
2899    #[test]
2900    fn slack_feedback_buttons_example_is_a_typed_context_actions_block(
2901    ) -> Result<(), Box<dyn std::error::Error>> {
2902        let message: serde_json::Value = serde_json::from_str(include_str!(
2903            "./fixtures/slack_feedback_buttons_message.json"
2904        ))?;
2905        let block = round_trip(&message["blocks"][0].to_string())?;
2906        let SlackBlock::ContextActions(context_actions) = block else {
2907            panic!("Expected a ContextActions block, got {block:?}");
2908        };
2909        assert!(matches!(
2910            context_actions.elements.as_slice(),
2911            [SlackContextActionBlockElement::FeedbackButtons(_)]
2912        ));
2913        Ok(())
2914    }
2915
2916    #[test]
2917    fn container_block_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2918        let block = round_trip(include_str!("./fixtures/slack_container_block.json"))?;
2919        let SlackBlock::Container(container) = block else {
2920            panic!("Expected a Container block, got {block:?}");
2921        };
2922        assert_eq!(container.is_collapsible, Some(true));
2923        assert_eq!(container.child_blocks.len(), 6);
2924        assert!(matches!(container.child_blocks[0], SlackBlock::Section(_)));
2925        assert!(matches!(container.child_blocks[5], SlackBlock::Actions(_)));
2926        Ok(())
2927    }
2928
2929    #[test]
2930    fn plan_block_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2931        let block = round_trip(include_str!("./fixtures/slack_plan_block.json"))?;
2932        let SlackBlock::Plan(plan) = block else {
2933            panic!("Expected a Plan block, got {block:?}");
2934        };
2935        assert_eq!(plan.title, "Thinking completed");
2936        assert_eq!(plan.tasks.len(), 3);
2937        assert_eq!(plan.tasks[1].status, Some(SlackTaskCardStatus::Pending));
2938        assert!(plan.tasks[2].output.is_some());
2939        Ok(())
2940    }
2941
2942    #[test]
2943    fn data_table_block_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2944        let block = round_trip(include_str!("./fixtures/slack_data_table_block.json"))?;
2945        let SlackBlock::DataTable(table) = block else {
2946            panic!("Expected a DataTable block, got {block:?}");
2947        };
2948        assert_eq!(table.caption, "A Fabulous Table");
2949        assert_eq!(table.rows.len(), 4);
2950        assert_eq!(table.rows[0][0], "Name".into());
2951        assert!(matches!(table.rows[1][2], SlackDataTableCell::RichText(_)));
2952        Ok(())
2953    }
2954
2955    #[test]
2956    fn data_table_number_and_action_cells_round_trip() -> Result<(), Box<dyn std::error::Error>> {
2957        // `action_cell` example from the docs page plus a `raw_number` and a future cell kind.
2958        let payload = serde_json::json!([
2959            { "type": "raw_number", "value": 1.5, "text": "1.5" },
2960            {
2961                "type": "action_cell",
2962                "element": {
2963                    "type": "button",
2964                    "text": { "type": "plain_text", "text": "Mark done" },
2965                    "action_id": "mark_done",
2966                    "value": "task_123"
2967                },
2968                "fallback": { "type": "raw_text", "text": "Open" }
2969            },
2970            { "type": "sparkline", "points": [1, 2] }
2971        ]);
2972        let cells: Vec<SlackDataTableCell> = serde_json::from_value(payload.clone())?;
2973        assert!(matches!(cells[0], SlackDataTableCell::RawNumber(_)));
2974        let SlackDataTableCell::ActionCell(action) = &cells[1] else {
2975            panic!("Expected an ActionCell, got {:?}", cells[1]);
2976        };
2977        assert_eq!(action.fallback.as_deref(), Some(&"Open".into()));
2978        assert!(matches!(cells[2], SlackDataTableCell::Unknown(_)));
2979        assert_eq!(serde_json::to_value(&cells)?, payload);
2980        Ok(())
2981    }
2982
2983    #[test]
2984    fn data_visualization_pie_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
2985        let block = round_trip(include_str!(
2986            "./fixtures/slack_data_visualization_pie_block.json"
2987        ))?;
2988        let SlackBlock::DataVisualization(viz) = block else {
2989            panic!("Expected a DataVisualization block, got {block:?}");
2990        };
2991        let SlackChart::Pie(pie) = viz.chart else {
2992            panic!("Expected a pie chart, got {:?}", viz.chart);
2993        };
2994        assert_eq!(pie.segments.len(), 4);
2995        assert_eq!(pie.segments[0].value, 45.into());
2996        Ok(())
2997    }
2998
2999    #[test]
3000    fn data_visualization_bar_fixture_round_trips() -> Result<(), Box<dyn std::error::Error>> {
3001        let block = round_trip(include_str!(
3002            "./fixtures/slack_data_visualization_bar_block.json"
3003        ))?;
3004        let SlackBlock::DataVisualization(viz) = block else {
3005            panic!("Expected a DataVisualization block, got {block:?}");
3006        };
3007        let SlackChart::Bar(bar) = viz.chart else {
3008            panic!("Expected a bar chart, got {:?}", viz.chart);
3009        };
3010        assert_eq!(bar.series[0].data.len(), 5);
3011        assert_eq!(bar.axis_config.categories.len(), 5);
3012        assert_eq!(
3013            bar.axis_config.y_label.as_deref(),
3014            Some("Percentage of Tastiness")
3015        );
3016        Ok(())
3017    }
3018
3019    #[test]
3020    fn stream_chunk_blocks_carries_new_block_types() -> Result<(), Box<dyn std::error::Error>> {
3021        let payload = serde_json::json!({
3022            "type": "blocks",
3023            "blocks": [
3024                { "type": "plan", "title": "Plan", "tasks": [] },
3025                { "type": "some_future_block", "x": 1 }
3026            ]
3027        });
3028        let chunk: crate::api::SlackStreamChunk = serde_json::from_value(payload.clone())?;
3029        let crate::api::SlackStreamChunk::Blocks { blocks } = &chunk else {
3030            panic!("Expected a Blocks chunk, got {chunk:?}");
3031        };
3032        assert!(matches!(blocks[0], SlackBlock::Plan(_)));
3033        assert!(matches!(blocks[1], SlackBlock::Unknown(_)));
3034        assert_eq!(serde_json::to_value(&chunk)?, payload);
3035        Ok(())
3036    }
3037
3038    #[test]
3039    fn header_block_keeps_its_level() -> Result<(), Box<dyn std::error::Error>> {
3040        let payload = serde_json::json!({
3041            "type": "header",
3042            "text": { "type": "plain_text", "text": "Budget", "emoji": true },
3043            "level": 2
3044        });
3045        let block: SlackBlock = serde_json::from_value(payload.clone())?;
3046        let SlackBlock::Header(header) = &block else {
3047            panic!("Expected a header block, got {block:?}");
3048        };
3049        assert_eq!(header.level, Some(SlackHeaderLevel(2)));
3050        assert_eq!(serde_json::to_value(&block)?, payload);
3051        let plain: SlackBlock = SlackHeaderBlock::new(crate::pt!("Budget")).into();
3052        assert!(serde_json::to_value(&plain)?.get("level").is_none());
3053        Ok(())
3054    }
3055}