slack_blocks/blox.rs
1//! # XML macro builder support
2//!
3//! # Blocks
4//!
5//! [`blocks::Actions`] - `<`[`actions_block`]`>`
6//!
7//! [`blocks::Header`] - `<`[`header_block`]`>` or `<`[`h1`]`>`
8//!
9//! [`blocks::Block::Divider`] - `<`[`divider_block`]`>` or `<`[`hr`]`>`
10//!
11//! [`blocks::Section`] - `<`[`section_block`]`>`
12//!
13//! [`blocks::Input`] - `<`[`input_block`]`>`
14//!
15//! [`blocks::Context`] - `<`[`context_block`]`>`
16//!
17//! [`blocks::File`] - `<`[`file_block`]`>`
18//!
19//! [`blocks::Image`] - `<`[`img_block`]`>`
20//!
21//! # Block Elements
22//!
23//! [`elems::TextInput`] - `<`[`text_input`]`>`
24//!
25//! [`elems::Image`] - `<`[`img`]`>`
26//!
27//! [`elems::Button`] - `<`[`button`]`>`
28//!
29//! [`elems::Checkboxes`] - `<`[`checkboxes`]`>`
30//!
31//! [`elems::DatePicker`] - `<`[`date_picker`]`>`
32//!
33//! [`elems::Overflow`] - `<`[`overflow`]`>`
34//!
35//! [`elems::Radio`] - `<`[`radio_buttons`]`>`
36//!
37//! [`elems::select`] - `<`[`select`]`>`
38//!
39//! # Composition Objects
40//!
41//! [`compose::text`] - `<`[`text()`]`>`
42//!
43//! [`compose::opt`] - `<`[`option`]`>`
44//!
45//! [`compose::opt_group`] - `<`[`option_group`]`>`
46//!
47//! [`compose::Confirm`] - `<`[`confirm`]`>`
48//!
49//! # Example
50//! Using an example from Slack's Documentation:
51//! ```json
52//! {
53//! "type": "section",
54//! "text": {
55//! "text": "*Sally* has requested you set the deadline for the Nano launch project",
56//! "type": "mrkdwn"
57//! },
58//! "accessory": {
59//! "type": "datepicker",
60//! "action_id": "datepicker123",
61//! "initial_date": "1990-04-28",
62//! "placeholder": {
63//! "type": "plain_text",
64//! "text": "Select a date"
65//! }
66//! }
67//! }
68//! ```
69//! ```rust
70//! use slack_blocks::blox::*;
71//!
72//! let pick_date = blox! {
73//! <date_picker action_id="datepicker123"
74//! placeholder="Select a date"
75//! initial_date=(28, 4, 1990) />
76//! };
77//!
78//! let section = blox! {
79//! <section_block accessory=pick_date>
80//! <text kind=plain>"*Sally* has requested you set the deadline for the Nano launch project"</text>
81//! </section_block>
82//! };
83//! ```
84
85pub use elems::{button::Style::{Danger as btn_danger,
86 Primary as btn_primary},
87 select::build::{choose::{multi, single},
88 data_source::{conversations,
89 external,
90 public_channels,
91 static_,
92 users}}};
93pub use mox::mox as blox;
94pub use text::build::kind::{mrkdwn, plain};
95
96use crate::{blocks, compose, elems, text};
97
98/// Identity trait to appease the mox macro
99pub trait IntoChild: Sized {
100 /// Identity function
101 fn into_child(self) -> Self {
102 self
103 }
104}
105
106impl<T> IntoChild for T {}
107
108pub use blox_blocks::*;
109pub use blox_compose::*;
110pub use blox_elems::*;
111
112mod blox_blocks {
113 use super::*;
114
115 /// Dummy builder so [blocks::Block::Divider] can be built with XML macro
116 #[derive(Debug, Copy, Clone)]
117 pub struct DividerBuilder;
118 impl DividerBuilder {
119 /// Constructs [blocks::Block::Divider]
120 pub fn build(self) -> blocks::Block<'static> {
121 blocks::Block::Divider
122 }
123 }
124
125 /// # [`blocks::Actions`] - `<actions_block>`
126 ///
127 /// Build a [`blocks::Actions`]
128 ///
129 /// |Attribute|Type|Optional|Available as child|
130 /// |-|-|-|-|
131 /// |[`element`](blocks::actions::build::ActionsBuilder::element())|`impl Into<`[`blocks::actions::SupportedElement`]`>`|❌|✅|
132 /// |[`block_id`](blocks::actions::build::ActionsBuilder::block_id())|[`String`] or [`&str`]|✅|❌|
133 ///
134 /// ## Example
135 /// ```
136 /// use slack_blocks::{blocks::Actions, blox::*, elems::Button, text};
137 ///
138 /// let xml = blox! {
139 /// <actions_block>
140 /// <button action_id="foo">"Foo"</button>
141 /// <button action_id="bar">"Bar"</button>
142 /// </actions_block>
143 /// };
144 ///
145 /// let equivalent =
146 /// Actions::builder().element(Button::builder().action_id("foo")
147 /// .text("Foo")
148 /// .build())
149 /// .element(Button::builder().action_id("bar")
150 /// .text("Bar")
151 /// .build())
152 /// .build();
153 ///
154 /// assert_eq!(xml, equivalent);
155 /// ```
156 pub fn actions_block(
157 )
158 -> blocks::actions::build::ActionsBuilderInit<'static>
159 {
160 blocks::Actions::builder()
161 }
162
163 /// # [`blocks::Header`] - `<header_block>` or `<h1>`
164 ///
165 /// Build a [`blocks::Header`]
166 ///
167 /// # Attributes
168 /// |Attribute|Type|Optional|Available as child|
169 /// |-|-|-|-|
170 /// |[`text`](blocks::header::build::HeaderBuilder::text())|[`text::Plain`], [`String`], [`&str`]|❌|✅|
171 /// |[`block_id`](blocks::header::build::HeaderBuilder::block_id())|[`String`] or [`&str`]|✅|❌|
172 ///
173 /// ## Example
174 /// ```
175 /// use slack_blocks::{blocks::Header, blox::*};
176 ///
177 /// let xml = blox! {
178 /// <h1>"Foo"</h1>
179 /// };
180 ///
181 /// let equivalent = Header::builder().text("Foo").build();
182 ///
183 /// assert_eq!(xml, equivalent);
184 /// ```
185 pub fn header_block() -> blocks::header::build::HeaderBuilderInit<'static> {
186 blocks::Header::builder()
187 }
188
189 /// Alias for [`header_block`]
190 pub fn h1() -> blocks::header::build::HeaderBuilderInit<'static> {
191 blocks::Header::builder()
192 }
193
194 /// # [`blocks::Block::Divider`] - `<divider_block />` or `<hr />`
195 ///
196 /// Build a [`blocks::Block::Divider`]
197 ///
198 /// # Attributes
199 /// None
200 ///
201 /// ## Example
202 /// ```
203 /// use slack_blocks::{blocks::Block, blox::*};
204 ///
205 /// let xml = blox! {
206 /// <hr />
207 /// };
208 ///
209 /// let equivalent = Block::Divider;
210 ///
211 /// assert_eq!(xml, equivalent);
212 /// ```
213 pub fn divider_block() -> DividerBuilder {
214 DividerBuilder
215 }
216
217 /// Alias for [`divider_block`]
218 pub fn hr() -> DividerBuilder {
219 divider_block()
220 }
221
222 /// # [`blocks::Section`] - `<section_block>`
223 ///
224 /// Build a [`blocks::Section`]
225 ///
226 /// # Attributes
227 /// |Attribute|Type|Optional|Available as child|
228 /// |-|-|-|-|
229 /// |[`text`] | [`text::Plain`], [`text::Mrkdwn`], or [`text::Text`]|❌*|❌|
230 /// |[`field`] | [`text::Plain`], [`text::Mrkdwn`], or [`text::Text`]|❌*|✅|
231 /// |[`fields`] | [`IntoIterator`] over [`text::Text`] |❌*|❌|
232 /// |[`accessory`]| [`elems::BlockElement`] |✅ |❌|
233 /// |[`block_id`] | [`String`] or [`&str`] |✅ |❌|
234 ///
235 /// * `text`, `field(s)`, or both are required.
236 ///
237 /// [`text`]: blocks::section::build::SectionBuilder::text()
238 /// [`field`]: blocks::section::build::SectionBuilder::field()
239 /// [`fields`]: blocks::section::build::SectionBuilder::fields()
240 /// [`accessory`]: blocks::section::build::SectionBuilder::accessory()
241 /// [`block_id`]: blocks::section::build::SectionBuilder::block_id()
242 ///
243 /// ## Example
244 /// ```
245 /// use slack_blocks::{blocks::Section, blox::*, text};
246 ///
247 /// let section_text = blox! { <text kind=plain>"Foo"</text> };
248 ///
249 /// let xml = blox! {
250 /// <section_block text=section_text>
251 /// <text kind=mrkdwn>"Bar"</text>
252 /// </section_block>
253 /// };
254 ///
255 /// let equivalent = Section::builder().text(text::Plain::from("Foo"))
256 /// .field(text::Mrkdwn::from("Bar"))
257 /// .build();
258 ///
259 /// assert_eq!(xml, equivalent);
260 /// ```
261 pub fn section_block(
262 )
263 -> blocks::section::build::SectionBuilderInit<'static>
264 {
265 blocks::Section::builder()
266 }
267
268 /// # [`blocks::Input`] - `<input_block>`
269 ///
270 /// Build a [`blocks::Input`]
271 ///
272 /// # Attributes
273 /// |Attribute|Type|Optional|Available as child|
274 /// |-|-|-|-|
275 /// |[`label`](blocks::input::build::InputBuilder::label())|[`text::Plain`], [`text::Mrkdwn`], or [`text::Text`] ([`<text>`](super::text()))|❌|❌|
276 /// |[`element`](blocks::input::build::InputBuilder::element())|`impl Into<`[`blocks::input::SupportedElement`]`>`|❌|✅|
277 /// |[`block_id`](blocks::input::build::InputBuilder::block_id())|[`String`] or [`&str`]|✅|❌|
278 /// |[`hint`](blocks::input::build::InputBuilder::hint())|[`text::Plain`] ([`<text>`](super::text())), [`String`], or [`&str`]|✅|❌|
279 /// |[`dispatch_actions`](blocks::input::build::InputBuilder::dispatch_actions())|[`bool`]|✅|❌|
280 /// |[`optional`](blocks::input::build::InputBuilder::optional())|[`bool`]|✅|❌|
281 ///
282 /// ## Example
283 /// ```
284 /// use slack_blocks::{blocks::Input, blox::*, elems::TextInput, text};
285 ///
286 /// let xml = blox! {
287 /// <input_block label="foo">
288 /// <text_input action_id="input" />
289 /// </input_block>
290 /// };
291 ///
292 /// let equivalent =
293 /// Input::builder().label("foo")
294 /// .element(TextInput::builder().action_id("input").build())
295 /// .build();
296 ///
297 /// assert_eq!(xml, equivalent);
298 /// ```
299 pub fn input_block() -> blocks::input::build::InputBuilderInit<'static> {
300 blocks::Input::builder()
301 }
302
303 /// # [`blocks::Context`] - `<context_block>`
304 ///
305 /// Build a [`blocks::Context`]
306 ///
307 /// # Attributes
308 /// |Attribute|Type|Optional|Available as child|
309 /// |-|-|-|-|
310 /// |[`element`](blocks::context::build::ContextBuilder::element())|[`text::Text`] ([`<text>`](super::text())) or [`elems::Image`] ([`<img>`](super::img()))|❌|✅|
311 /// |[`block_id`](blocks::context::build::ContextBuilder::block_id())|[`String`] or [`&str`]|✅|❌|
312 ///
313 /// ## Example
314 /// ```
315 /// use slack_blocks::{blocks::Context, blox::*, elems::Image, text};
316 ///
317 /// let xml = blox! {
318 /// <context_block>
319 /// <text kind=plain>"Enjoy this picture of bar"</text>
320 /// <img src="https://foo.com/bar.png" alt="a pic of bar" />
321 /// </context_block>
322 /// };
323 ///
324 /// let equivalent =
325 /// Context::builder().element(text::Plain::from("Enjoy this picture of bar"))
326 /// .element(Image::builder().src("https://foo.com/bar.png")
327 /// .alt("a pic of bar")
328 /// .build())
329 /// .build();
330 ///
331 /// assert_eq!(xml, equivalent);
332 /// ```
333 pub fn context_block(
334 )
335 -> blocks::context::build::ContextBuilderInit<'static>
336 {
337 blocks::Context::builder()
338 }
339
340 /// # [`blocks::File`] - `<file_block>`
341 ///
342 /// Build a [`blocks::File`]
343 ///
344 /// # Attributes
345 /// |Attribute|Type|Optional|Available as child|
346 /// |-|-|-|-|
347 /// |[`external_id`](blocks::file::build::FileBuilder::external_id())|[`String`] or [`&str`]|❌|✅|
348 /// |[`block_id`](blocks::file::build::FileBuilder::block_id())|[`String`] or [`&str`]|✅|❌|
349 ///
350 /// ## Example
351 /// ```
352 /// use slack_blocks::{blocks::File, blox::*};
353 ///
354 /// let xml = blox! {
355 /// <file_block external_id="foo" />
356 /// };
357 ///
358 /// let equivalent = File::builder().external_id("foo").build();
359 ///
360 /// assert_eq!(xml, equivalent);
361 /// ```
362 pub fn file_block() -> blocks::file::build::FileBuilderInit<'static> {
363 blocks::File::builder()
364 }
365
366 /// # [`blocks::Image`] - `<img_block>`
367 ///
368 /// Build a [`blocks::Image`]
369 ///
370 /// # Attributes
371 /// |Attribute|Type|Optional|Available as child|
372 /// |-|-|-|-|
373 /// |[`src`](blocks::image::build::ImageBuilder::src())|[`String`] or [`&str`]|❌|❌|
374 /// |[`alt`](blocks::image::build::ImageBuilder::alt())|[`String`] or [`&str`]|❌|❌|
375 /// |[`block_id`](blocks::file::build::FileBuilder::block_id())|[`String`] or [`&str`]|✅|❌|
376 ///
377 /// ## Example
378 /// ```
379 /// use slack_blocks::{blocks::Image, blox::*};
380 ///
381 /// let xml = blox! {
382 /// <img_block src="https://foo.com/bar.png" alt="a pic of bar" />
383 /// };
384 ///
385 /// let equivalent = Image::builder().src("https://foo.com/bar.png")
386 /// .alt("a pic of bar")
387 /// .build();
388 ///
389 /// assert_eq!(xml, equivalent);
390 /// ```
391 pub fn img_block() -> blocks::image::build::ImageBuilderInit<'static> {
392 blocks::Image::builder()
393 }
394}
395
396mod blox_elems {
397 use super::*;
398
399 /// # [`elems::TextInput`] - `<text_input>`
400 ///
401 /// Build a [`elems::TextInput`]
402 ///
403 /// # Attributes
404 /// |Attribute|Type|Optional|Available as child|
405 /// |-|-|-|-|
406 /// |[`action_id`] | [`String`] or [`&str`] |❌|❌|
407 /// |[`action_trigger`] | [`elems::text_input::ActionTrigger`] |✅|❌|
408 /// |[`placeholder`] | [`text::Plain`] ([`<text>`](super::text())), [`String`] or [`&str`] |✅|❌|
409 /// |[`initial_value`] | [`String`] or [`&str`] |✅|❌|
410 /// |[`length`] | impl [`std::ops::RangeBounds`] over [`u32`] |✅|❌|
411 /// |[`min_length`] | [`u32`] |✅|❌|
412 /// |[`max_length`] | [`u32`] |✅|❌|
413 /// |[`multiline`] | [`bool`] |✅|❌|
414 ///
415 /// [`action_id`]: elems::text_input::build::TextInputBuilder::action_id()
416 /// [`action_trigger`]: elems::text_input::build::TextInputBuilder::action_trigger()
417 /// [`placeholder`]: elems::text_input::build::TextInputBuilder::placeholder()
418 /// [`initial_value`]: elems::text_input::build::TextInputBuilder::initial_value()
419 /// [`length`]: elems::text_input::build::TextInputBuilder::length()
420 /// [`min_length`]: elems::text_input::build::TextInputBuilder::min_length()
421 /// [`max_length`]: elems::text_input::build::TextInputBuilder::max_length()
422 /// [`multiline`]: elems::text_input::build::TextInputBuilder::multiline()
423 ///
424 /// ## Example
425 /// ```
426 /// use slack_blocks::{blox::*, elems::TextInput};
427 ///
428 /// let xml: TextInput = blox! {
429 /// <text_input action_id="name_input"
430 /// multiline=true
431 /// placeholder="Type your name"
432 /// length={1..=1000}
433 /// />
434 /// };
435 ///
436 /// let equiv = TextInput::builder().action_id("name_input")
437 /// .multiline(true)
438 /// .placeholder("Type your name")
439 /// .length(1..=1000)
440 /// .build();
441 ///
442 /// assert_eq!(xml, equiv)
443 /// ```
444 pub fn text_input(
445 )
446 -> elems::text_input::build::TextInputBuilderInit<'static>
447 {
448 elems::TextInput::builder()
449 }
450
451 /// # [`elems::Image`] - `<img />`
452 ///
453 /// Build a [`elems::Image`]
454 ///
455 /// # Attributes
456 /// |Attribute|Type|Optional|Available as child|
457 /// |-|-|-|-|
458 /// |[`src`](elems::image::build::ImageBuilder::src()) | [`String`] or [`&str`] |❌|❌|
459 /// |[`alt`](elems::image::build::ImageBuilder::alt()) | [`String`] or [`&str`] |❌|❌|
460 ///
461 /// ## Example
462 /// ```
463 /// use slack_blocks::{blox::*, elems::Image};
464 ///
465 /// let xml: Image = blox! {
466 /// <img src="https://foo.com/bar.png" alt="a pic of bar" />
467 /// };
468 ///
469 /// let equiv = Image::builder().src("https://foo.com/bar.png")
470 /// .alt("a pic of bar")
471 /// .build();
472 ///
473 /// assert_eq!(xml, equiv)
474 /// ```
475 pub fn img() -> elems::image::build::ImageBuilderInit<'static> {
476 elems::Image::builder()
477 }
478
479 /// # [`elems::Button`] - `<button>`
480 ///
481 /// Build a [`elems::Button`]
482 ///
483 /// # Attributes
484 /// |Attribute|Type|Optional|Available as child|
485 /// |-|-|-|-|
486 /// |[`action_id`] | [`String`] or [`&str`] |❌|❌|
487 /// |[`text`] | [`text::Plain`] ([`<text>`](super::text())), [`String`] or [`&str`] |❌|✅|
488 /// |[`url`] | [`String`] or [`&str`] |✅|❌|
489 /// |[`value`] | [`String`] or [`&str`] |✅|❌|
490 /// |[`style`] | [`elems::button::Style`] ([`btn_primary`] or [`btn_danger`]) |✅|❌|
491 /// |[`confirm`] | [`compose::Confirm`] ([`<confirm>`](super::confirm())) |✅|❌|
492 ///
493 /// [`action_id`]: elems::button::build::ButtonBuilder::action_id()
494 /// [`text`]: elems::button::build::ButtonBuilder::text()
495 /// [`url`]: elems::button::build::ButtonBuilder::url()
496 /// [`value`]: elems::button::build::ButtonBuilder::value()
497 /// [`style`]: elems::button::build::ButtonBuilder::style()
498 /// [`confirm`]: elems::button::build::ButtonBuilder::confirm()
499 ///
500 /// ## Example
501 /// ```
502 /// use slack_blocks::{blox::*,
503 /// elems::button::{Button, Style}};
504 ///
505 /// let xml: Button = blox! {
506 /// <button action_id="dangerous" style=btn_danger>"DANGER!"</button>
507 /// };
508 ///
509 /// let equiv = Button::builder().action_id("dangerous")
510 /// .text("DANGER!")
511 /// .style(Style::Danger)
512 /// .build();
513 ///
514 /// assert_eq!(xml, equiv)
515 /// ```
516 pub fn button() -> elems::button::build::ButtonBuilderInit<'static> {
517 elems::Button::builder()
518 }
519
520 /// # [`elems::Checkboxes`] - `<checkboxes>`
521 ///
522 /// Build a [`elems::Checkboxes`]
523 ///
524 /// # Attributes
525 /// |Attribute|Type|Optional|Available as child|
526 /// |-|-|-|-|
527 /// |[`action_id`] | [`String`] or [`&str`] |❌|❌|
528 /// |[`option`] | [`compose::Opt`] ([`<option>`](super::option()))* |❌ _(or `options`)_|✅|
529 /// |[`options`] | [`Vec`] of [`compose::Opt`]* |❌ _(or `option`)_|❌|
530 /// |[`initial_options`] | [`Vec`] of [`compose::Opt`]* provided via [`option`] or [`options`]. |✅|❌|
531 /// |[`confirm`] | [`compose::Confirm`] ([`<confirm>`](super::confirm())) |✅|❌|
532 ///
533 /// [`action_id`]: elems::checkboxes::build::CheckboxesBuilder::action_id()
534 /// [`option`]: elems::checkboxes::build::CheckboxesBuilder::option()
535 /// [`options`]: elems::checkboxes::build::CheckboxesBuilder::options()
536 /// [`initial_options`]: elems::checkboxes::build::CheckboxesBuilder::initial_options()
537 /// [`confirm`]: elems::checkboxes::build::CheckboxesBuilder::confirm()
538 ///
539 /// * Options cannot have URL set, option text can be mrkdwn or plain.
540 ///
541 /// ## Example
542 /// ```
543 /// use slack_blocks::{blox::*, compose::Opt, elems::Checkboxes};
544 ///
545 /// let xml: Checkboxes = blox! {
546 /// <checkboxes action_id="chex">
547 /// <option value="check_1">
548 /// <text kind=plain>"Foo"</text>
549 /// </option>
550 /// <option value="check_2">
551 /// <text kind=mrkdwn>"Bar"</text>
552 /// </option>
553 /// <option value="check_3">
554 /// <text kind=plain>"Zoo"</text>
555 /// </option>
556 /// </checkboxes>
557 /// };
558 ///
559 /// let equiv = Checkboxes::builder().action_id("chex")
560 /// .option(Opt::builder().value("check_1")
561 /// .text_plain("Foo")
562 /// .build())
563 /// .option(Opt::builder().value("check_2")
564 /// .text_md("Bar")
565 /// .build())
566 /// .option(Opt::builder().value("check_3")
567 /// .text_plain("Zoo")
568 /// .build())
569 /// .build();
570 ///
571 /// assert_eq!(xml, equiv)
572 /// ```
573 pub fn checkboxes(
574 )
575 -> elems::checkboxes::build::CheckboxesBuilderInit<'static>
576 {
577 elems::Checkboxes::builder()
578 }
579
580 /// # [`elems::DatePicker`] - `<date_picker>`
581 ///
582 /// Build a [`elems::DatePicker`]
583 ///
584 /// # Attributes
585 /// |Attribute|Type|Optional|Available as child|
586 /// |-|-|-|-|
587 /// |[`action_id`] | [`String`] or [`&str`] |❌|❌|
588 /// |[`placeholder`] | [`String`] or [`&str`] |✅|❌|
589 /// |[`initial_date`] | ([`u8`] _day_, [`u8`] _month_, [`u16`] _year_) |✅|❌|
590 /// |[`confirm`] | [`compose::Confirm`] ([`<confirm>`](super::confirm())) |✅|❌|
591 ///
592 /// [`action_id`]: elems::date_picker::build::DatePickerBuilder::action_id()
593 /// [`placeholder`]: elems::date_picker::build::DatePickerBuilder::placeholder()
594 /// [`initial_date`]: elems::date_picker::build::DatePickerBuilder::initial_date()
595 /// [`confirm`]: elems::date_picker::build::DatePickerBuilder::confirm()
596 ///
597 /// ## Example
598 /// ```
599 /// use slack_blocks::{blox::*, elems::DatePicker};
600 ///
601 /// let xml = blox! {
602 /// <date_picker action_id="pick_birthday" placeholder="Pick your birthday!" />
603 /// };
604 ///
605 /// let equiv = DatePicker::builder().action_id("pick_birthday")
606 /// .placeholder("Pick your birthday!")
607 /// .build();
608 ///
609 /// assert_eq!(xml, equiv)
610 /// ```
611 pub fn date_picker(
612 )
613 -> elems::date_picker::build::DatePickerBuilderInit<'static>
614 {
615 elems::DatePicker::builder()
616 }
617
618 /// # [`elems::Overflow`] - `<overflow>`
619 ///
620 /// Build a [`elems::Overflow`]
621 ///
622 /// # Attributes
623 /// |Attribute|Type|Optional|Available as child|
624 /// |-|-|-|-|
625 /// |[`action_id`] | [`String`] or [`&str`] |❌|❌|
626 /// |[`option`] | [`compose::Opt`] ([`<option>`](super::option()))* |❌ _(or options)_|✅|
627 /// |[`options`] | [`Vec`] of [`compose::Opt`]* |❌ _(or option)_|❌|
628 /// |[`confirm`] | [`compose::Confirm`] ([`<confirm>`](super::confirm())) |✅|❌|
629 ///
630 /// [`action_id`]: elems::overflow::build::OverflowBuilder::action_id()
631 /// [`option`]: elems::overflow::build::OverflowBuilder::option()
632 /// [`options`]: elems::overflow::build::OverflowBuilder::options()
633 /// [`confirm`]: elems::overflow::build::OverflowBuilder::confirm()
634 ///
635 /// * Options **can** have URL set, option text must be plain.
636 ///
637 /// ## Example
638 /// ```
639 /// use slack_blocks::{blox::*, compose::Opt, elems::Overflow};
640 ///
641 /// let xml = blox! {
642 /// <overflow action_id="menu">
643 /// <option value="foo"><text kind=plain>"Foo"</text></option>
644 /// <option value="url" text_plain="Open link" url="foo.com" />
645 /// </overflow>
646 /// };
647 ///
648 /// let equiv = Overflow::builder().action_id("menu")
649 /// .option(Opt::builder().value("foo")
650 /// .text_plain("Foo")
651 /// .build())
652 /// .option(Opt::builder().value("url")
653 /// .text_plain("Open link")
654 /// .url("foo.com")
655 /// .build())
656 /// .build();
657 ///
658 /// assert_eq!(xml, equiv)
659 /// ```
660 pub fn overflow() -> elems::overflow::build::OverflowBuilderInit<'static> {
661 elems::Overflow::builder()
662 }
663
664 /// # [`elems::Radio`] - `<radio_buttons>`
665 ///
666 /// Build a [`elems::Radio`]
667 ///
668 /// # Attributes
669 /// |Attribute|Type|Optional|Available as child|
670 /// |-|-|-|-|
671 /// |[`action_id`] | [`String`] or [`&str`] |❌|❌|
672 /// |[`option`] | [`compose::Opt`] ([`<option>`](super::option()))* |❌ _(or options)_|✅|
673 /// |[`options`] | [`Vec`] of [`compose::Opt`]* |❌ _(or option)_|❌|
674 /// |[`initial_option`] | [`compose::Opt`]* |✅|❌|
675 /// |[`confirm`] | [`compose::Confirm`] ([`<confirm>`](super::confirm())) |✅|❌|
676 ///
677 /// [`action_id`]: elems::radio::build::RadioBuilder::action_id()
678 /// [`option`]: elems::radio::build::RadioBuilder::option()
679 /// [`options`]: elems::radio::build::RadioBuilder::options()
680 /// [`initial_option`]: elems::radio::build::RadioBuilder::initial_option()
681 /// [`confirm`]: elems::radio::build::RadioBuilder::confirm()
682 ///
683 /// * Options **cannot** have URL set, option text can be mrkdwn or plain.
684 ///
685 /// ## Example
686 /// ```
687 /// use slack_blocks::{blocks::Input, blox::*, compose::Opt, elems::Radio};
688 ///
689 /// let xml = blox! {
690 /// <input_block label="Pick your favorite cheese!">
691 /// <radio_buttons action_id="cheese_picker">
692 /// <option value="feta"><text kind=plain>"Feta"</text></option>
693 /// <option value="gouda"><text kind=plain>"Gouda"</text></option>
694 /// <option value="cheddar"><text kind=plain>"Cheddar"</text></option>
695 /// </radio_buttons>
696 /// </input_block>
697 /// };
698 ///
699 /// let equiv = {
700 /// let feta = Opt::builder().value("feta").text_plain("Feta").build();
701 ///
702 /// let gouda = Opt::builder().value("gouda").text_plain("Gouda").build();
703 ///
704 /// let cheddar = Opt::builder().value("cheddar")
705 /// .text_plain("Cheddar")
706 /// .build();
707 ///
708 /// let radio = Radio::builder().action_id("cheese_picker")
709 /// .option(feta)
710 /// .option(gouda)
711 /// .option(cheddar)
712 /// .build();
713 ///
714 /// Input::builder().label("Pick your favorite cheese!")
715 /// .element(radio)
716 /// .build()
717 /// };
718 ///
719 /// assert_eq!(xml, equiv)
720 /// ```
721 pub fn radio_buttons() -> elems::radio::build::RadioBuilderInit<'static> {
722 elems::Radio::builder()
723 }
724
725 /// # [`elems::select`] - `<select>`
726 ///
727 /// Build a [`elems::select`].
728 ///
729 /// # Attributes
730 ///
731 /// This element behaves like a flow chart, allowing you to construct all
732 /// Single and Multi-selects with a single XML element.
733 ///
734 /// Use the `kind` attribute to distinguish between
735 /// single- and multi-selects.
736 ///
737 /// Use the `choose_from` attribute to distinguish between
738 /// the many sources of options for the select menu.
739 ///
740 /// ## Common
741 /// |Attribute|Type|Optional|Available as child|
742 /// |-|-|-|-|
743 /// |[`kind`] | [`single`] (default) or [`multi`] |✅|❌|
744 /// |[`choose_from`] | [`users`], [`public_channels`], [`static_`], [`external`], [`conversations`] |❌|❌|
745 /// |`action_id` | [`String`] or [`&str`] |❌|❌|
746 /// |`placeholder` | [`String`] or [`&str`] |✅|❌|
747 /// |`confirm` | [`compose::Confirm`] ([`<confirm>`](super::confirm())) |✅|❌|
748 /// |**when kind=multi**<br/>`max_selected_items` | [`u32`] |✅|❌|
749 ///
750 /// [`kind`]: elems::select::build::SelectBuilder::kind()
751 /// [`choose_from`]: elems::select::build::SelectBuilder::choose_from()
752 ///
753 /// ## **When `choose_from=users`**
754 /// |Attribute|Type|Optional|Available as child|
755 /// |-|-|-|-|
756 /// |**when kind=single**<br/>[`initial_user`]| [`String`] or [`&str`] |✅|❌|
757 /// |**when kind=multi**<br/>[`initial_users`]| impl [`IntoIterator`] over [`String`] or [`&str`] |✅|❌|
758 ///
759 /// [`initial_user`]: elems::select::user::build::UserBuilder::initial_user()
760 /// [`initial_users`]: elems::select::user::build::UserBuilder::initial_users()
761 ///
762 /// ## **When `choose_from=public_channels`**
763 /// |Attribute|Type|Optional|Available as child|
764 /// |-|-|-|-|
765 /// |**when kind=single**<br/>[`initial_channel`]| [`String`] or [`&str`] |✅|❌|
766 /// |**when kind=multi**<br/>[`initial_channels`]| impl [`IntoIterator`] over [`String`] or [`&str`] |✅|❌|
767 ///
768 /// [`initial_channel`]: elems::select::public_channel::build::PublicChannelBuilder::initial_channel()
769 /// [`initial_channels`]: elems::select::public_channel::build::PublicChannelBuilder::initial_channels()
770 ///
771 /// ## **When `choose_from=external`**
772 /// |Attribute|Type|Optional|Available as child|
773 /// |-|-|-|-|
774 /// |[`min_query_length`] | [`u64`] |✅|❌|
775 /// |**when kind=single**<br/>[`initial_option`]| [`compose::Opt`]* |✅|❌|
776 /// |**when kind=single**<br/>[`initial_option_group`]| [`compose::OptGroup`]* |✅|❌|
777 /// |**when kind=multi**<br/>[`initial_options`]| impl [`IntoIterator`] over [`compose::Opt`]s* |✅|❌|
778 /// |**when kind=multi**<br/>[`initial_option_groups`]| impl [`IntoIterator`] over [`compose::OptGroup`]s* |✅|❌|
779 ///
780 /// * Options **cannot** have URL set, option text must be plain.
781 ///
782 /// [`initial_option`]: elems::select::external::build::ExternalBuilder::initial_option()
783 /// [`initial_option_group`]: elems::select::external::build::ExternalBuilder::initial_option_group()
784 /// [`initial_option_groups`]: elems::select::external::build::ExternalBuilder::initial_option_groups()
785 /// [`initial_options`]: elems::select::external::build::ExternalBuilder::initial_options()
786 /// [`min_query_length`]: elems::select::external::build::ExternalBuilder::min_query_length()
787 ///
788 /// ## **When `choose_from=conversations`**
789 /// |Attribute|Type|Optional|Available as child|
790 /// |-|-|-|-|
791 /// |[`filter`] | [`compose::ConversationFilter`] |✅|❌|
792 /// |[`initial_channel_current`] | [`bool`] |✅|❌|
793 /// |**when kind=single**<br/>[`initial_channel`]| [`String`] or [`&str`] |✅|❌|
794 /// |**when kind=multi**<br/>[`initial_channels`]| impl [`IntoIterator`] over [`String`]s or [`&str`]s |✅|❌|
795 ///
796 /// [`filter`]: elems::select::conversation::build::ConversationBuilder::filter()
797 /// [`initial_channel_current`]: elems::select::conversation::build::ConversationBuilder::initial_channel_current()
798 /// [`initial_channel`]: elems::select::conversation::build::ConversationBuilder::initial_channel()
799 /// [`initial_channels`]: elems::select::conversation::build::ConversationBuilder::initial_channels()
800 ///
801 /// ## **When `choose_from=static_`**
802 /// This is the most complex select menu.
803 ///
804 /// It allows you to define the options in your app
805 /// by using children or `options`/`option_groups`.
806 ///
807 /// Note that all children must **either** be `<option>`s or `<option_group>`s.
808 /// You cannot mix options and groups in the same static select menu.
809 ///
810 /// For single-selects that use options, you can pre-select one of them with `initial_option`.
811 ///
812 /// For multi-selects that use options, you can pre-select any number of them with `initial_options`.
813 ///
814 /// For single-selects that use option groups, you can pre-select one of them with `initial_option_group`.
815 ///
816 /// For multi-selects that use option groups, you can pre-select any number of them with `initial_option_groups`.
817 ///
818 /// |Attribute|Type|Optional|Available as child|
819 /// |-|-|-|-|
820 /// | Child | [`compose::Opt`] ([`<option>`](super::option())) or [`compose::OptGroup`] ([`<option_group>`](super::option_group()))* |❌ _(or `options` or `option_groups`)_|✅|
821 /// |[`option_groups`] | [`Vec`] of [`compose::OptGroup`]* |❌ _(or Child or `option_groups`)_|❌|
822 /// |[`options`] | [`Vec`] of [`compose::Opt`]* |❌ _(or Child or `options`)_ |❌|
823 /// |**when kind=single**<br/>**and select uses Opts**<br/>(not OptGroups)<br/>[`initial_option`]| [`compose::Opt`]* |✅|❌|
824 /// |**when kind=single**<br/>**and select uses OptGroup**<br/>(not Opts)<br/>[`initial_option_group`]| [`compose::OptGroup`]* |✅|❌|
825 /// |**when kind=multi**<br/>**and select uses Opt**<br/>(not OptGroups)<br/>[`initial_options`]| impl [`IntoIterator`] over [`compose::Opt`]* |✅|❌|
826 /// |**when kind=multi**<br/>**and select uses OptGroup**<br/>(not Opts)<br/>[`initial_option_groups`]| impl [`IntoIterator`] over [`compose::OptGroup`]* |✅|❌|
827 ///
828 /// * All children must either be options or option groups. Options **cannot** have URL set, option text must be plain.
829 ///
830 /// [`initial_option`]: elems::select::static_::build::StaticBuilder::initial_option()
831 /// [`initial_options`]: elems::select::static_::build::StaticBuilder::initial_options()
832 /// [`initial_option_group`]: elems::select::static_::build::StaticBuilder::initial_option_group()
833 /// [`initial_option_groups`]: elems::select::static_::build::StaticBuilder::initial_option_groups()
834 /// [`options`]: elems::select::static_::build::StaticBuilder::options()
835 /// [`option_groups`]: elems::select::static_::build::StaticBuilder::option_groups()
836 ///
837 /// # Examples
838 ///
839 /// ## `choose_from=users`
840 /// ```
841 /// use slack_blocks::elems::select;
842 /// use slack_blocks::blox::*;
843 ///
844 /// let xml = blox! {
845 /// <select kind=multi choose_from=users placeholder="Pick some users!" action_id="foo" />
846 /// };
847 ///
848 /// let equiv = select::multi::User::builder().placeholder("Pick some users!").action_id("foo").build();
849 ///
850 /// assert_eq!(xml, equiv)
851 /// ```
852 ///
853 /// ## `choose_from=static_`
854 /// ```
855 /// use slack_blocks::{elems::select, compose::Opt, blox::*};
856 ///
857 /// let xml = blox! {
858 /// <select choose_from=static_ placeholder="Pick your favorite cheese!" action_id="foo">
859 /// <option value="gouda"><text kind=plain>"Gouda"</text></option>
860 /// <option value="feta"><text kind=plain>"Feta"</text></option>
861 /// <option value="cheddar"><text kind=plain>"Cheddar"</text></option>
862 /// <option value="fontina"><text kind=plain>"Fontina"</text></option>
863 /// </select>
864 /// };
865 ///
866 /// let equiv = select::Static::builder().placeholder("Pick your favorite cheese!")
867 /// .action_id("foo")
868 /// .option(Opt::builder().value("gouda").text_plain("Gouda").build())
869 /// .option(Opt::builder().value("feta").text_plain("Feta").build())
870 /// .option(Opt::builder().value("cheddar").text_plain("Cheddar").build())
871 /// .option(Opt::builder().value("fontina").text_plain("Fontina").build())
872 /// .build();
873 ///
874 /// assert_eq!(xml, equiv)
875 /// ```
876 pub fn select() -> elems::select::build::SelectBuilderInit {
877 elems::select::build::SelectBuilderInit::new()
878 }
879}
880
881mod blox_compose {
882 use super::*;
883
884 /// # [`compose::text`] - `<text>`
885 ///
886 /// Build a [`compose::text`]
887 ///
888 /// # Attributes
889 /// |Attribute|Type|Optional|Available as child|
890 /// |-|-|-|-|
891 /// |[`kind`] | [`mrkdwn`] or [`plain`] |❌|❌|
892 /// |[`text`] | [`&str`] or [`String`] |❌|✅|
893 /// |**When kind=plain**<br/>[`emoji`] | [`bool`] |✅|❌|
894 /// |**When kind=mrkdwn**<br/>[`verbatim`] | [`bool`] |✅|❌|
895 ///
896 /// [`kind`]: compose::text::build::TextBuilder::kind()
897 /// [`text`]: compose::text::build::TextBuilder::text()
898 /// [`emoji`]: compose::text::build::TextBuilder::emoji()
899 /// [`verbatim`]: compose::text::build::TextBuilder::verbatim()
900 ///
901 /// ## Example
902 /// ```
903 /// use slack_blocks::{blocks::Section, blox::*, text};
904 ///
905 /// let xml = blox! {
906 /// <text kind=plain>"Foo"</text>
907 /// };
908 ///
909 /// let equivalent = text::Plain::from("Foo");
910 ///
911 /// assert_eq!(xml, equivalent);
912 /// ```
913 pub fn text() -> text::build::TextBuilderInit {
914 text::Text::builder()
915 }
916
917 /// # [`compose::opt`] - `<option>`
918 ///
919 /// Build a [`compose::opt`]
920 ///
921 /// # Attributes
922 /// |Attribute|Type|Optional|Available as child|
923 /// |-|-|-|-|
924 /// |[`value`] | [`&str`] or [`String`] |❌|❌|
925 /// |[`text`] | [`text::Plain`] or [`text::Mrkdwn`] ([`<text>`](super::text())) |❌|✅|
926 /// |[`desc`] | [`&str`], [`String`], or [`text::Plain`] |✅|❌|
927 /// |[`url`] | [`&str`] or [`String`] |✅|❌|
928 ///
929 /// [`value`]: compose::opt::build::OptBuilder::value()
930 /// [`text`]: compose::opt::build::OptBuilder::text()
931 /// [`desc`]: compose::opt::build::OptBuilder::desc()
932 /// [`url`]: compose::opt::build::OptBuilder::url()
933 ///
934 /// ## Example
935 /// ```
936 /// use slack_blocks::{blox::*, compose::Opt};
937 ///
938 /// let xml = blox! {
939 /// <option value="foo">
940 /// <text kind=plain>"Foo"</text>
941 /// </option>
942 /// };
943 ///
944 /// let equivalent = Opt::builder().text_plain("Foo").value("foo").build();
945 ///
946 /// assert_eq!(xml, equivalent);
947 /// ```
948 pub fn option() -> compose::opt::build::OptBuilderInit<'static> {
949 compose::Opt::builder()
950 }
951
952 /// # [`compose::opt_group`] - `<option_group>`
953 ///
954 /// Build a [`compose::opt_group`]
955 ///
956 /// # Attributes
957 /// |Attribute|Type|Optional|Available as child|
958 /// |-|-|-|-|
959 /// |[`label`] | [`&str`], [`String`], or [`text::Plain`] |❌|❌|
960 /// |[`option`] | [`compose::Opt`] |❌ _(or options)_|✅|
961 /// |[`options`] | impl [`IntoIterator`] over [`compose::Opt`] |❌ _(or option)_|❌|
962 ///
963 /// [`label`]: compose::opt_group::build::OptGroupBuilder::label()
964 /// [`option`]: compose::opt_group::build::OptGroupBuilder::option()
965 /// [`options`]: compose::opt_group::build::OptGroupBuilder::options()
966 ///
967 /// Supports up to 100 option objects of the same type.
968 ///
969 /// Note that `syn-rsx` (and `mox` by extension)
970 /// do not support using iterables as children.
971 ///
972 /// This means that if you have a `Vec<Opt>`,
973 /// you'll need to pass it to an `options` attribute instead.
974 ///
975 /// See Examples for an example of compile-time child opts, and runtime (Vec) opts.
976 ///
977 /// ## Example - Options known at compile-time
978 /// ```
979 /// use slack_blocks::{blox::*,
980 /// compose::{Opt, OptGroup}};
981 ///
982 /// let xml = blox! {
983 /// <option_group label="foos_and_bars">
984 /// <option value="foo"><text kind=plain>"Foo"</text></option>
985 /// <option value="bar"><text kind=plain>"Bar"</text></option>
986 /// </option_group>
987 /// };
988 ///
989 /// let equivalent = OptGroup::builder().label("foos_and_bars")
990 /// .option(Opt::builder().value("foo")
991 /// .text_plain("Foo")
992 /// .build())
993 /// .option(Opt::builder().value("bar")
994 /// .text_plain("Bar")
995 /// .build())
996 /// .build();
997 ///
998 /// assert_eq!(xml, equivalent);
999 /// ```
1000 ///
1001 /// ## Example - Dynamic vec of options
1002 /// ```
1003 /// use slack_blocks::{blox::*,
1004 /// compose::{Opt, OptGroup}};
1005 ///
1006 /// # fn uuid() -> String {"foo".to_string()}
1007 /// # fn random_word() -> String {"foo".to_string()}
1008 /// let generate_option = || {
1009 /// blox! {
1010 /// <option value={uuid()}>
1011 /// <text kind=plain>{random_word()}</text>
1012 /// </option>
1013 /// }
1014 /// };
1015 ///
1016 /// // Generate 80 random options
1017 /// let options = std::iter::repeat(()).take(80)
1018 /// .map(|_| generate_option())
1019 /// .collect::<Vec<_>>();
1020 ///
1021 /// let xml = blox! {
1022 /// <option_group label="foos_and_bars" options={options.clone()} />
1023 /// };
1024 ///
1025 /// let equivalent = OptGroup::builder().label("foos_and_bars")
1026 /// .options(options.clone())
1027 /// .build();
1028 ///
1029 /// assert_eq!(xml, equivalent);
1030 /// ```
1031 pub fn option_group(
1032 )
1033 -> compose::opt_group::build::OptGroupBuilderInit<'static>
1034 {
1035 compose::OptGroup::builder()
1036 }
1037
1038 /// # [`compose::Confirm`] - `<confirm>`
1039 ///
1040 /// Build a [`compose::Confirm`]
1041 ///
1042 /// # Attributes
1043 /// |Attribute|Type|Optional|Available as child|
1044 /// |-|-|-|-|
1045 /// |[`title`] | [`&str`], [`String`], or [`text::Plain`] |❌|❌|
1046 /// |[`text`], [`text_plain`], or [`text_md`]|`text` wants [`text::Plain`] or [`text::Mrkdwn`]. `text_md` & `text_plain` want [`&str`] or [`String`]. |❌|❌|
1047 /// |[`confirm`] | [`&str`], [`String`], or [`text::Plain`] |❌|❌|
1048 /// |[`deny`] | [`&str`], [`String`], or [`text::Plain`] |❌|❌|
1049 /// |[`style`] | [`compose::confirm::ConfirmStyle`] |✅|❌|
1050 ///
1051 /// [`title`]: compose::confirm::build::ConfirmBuilder::title()
1052 /// [`text`]: compose::confirm::build::ConfirmBuilder::text()
1053 /// [`text_plain`]: compose::confirm::build::ConfirmBuilder::text_plain()
1054 /// [`text_md`]: compose::confirm::build::ConfirmBuilder::text_md()
1055 /// [`confirm`]: compose::confirm::build::ConfirmBuilder::confirm()
1056 /// [`deny`]: compose::confirm::build::ConfirmBuilder::deny()
1057 /// [`style`]: compose::confirm::build::ConfirmBuilder::style()
1058 ///
1059 /// ## Example
1060 /// ```
1061 /// use slack_blocks::{blox::*, compose::Confirm, text::ToSlackPlaintext};
1062 ///
1063 /// let xml = blox! {
1064 /// <confirm title="Title"
1065 /// text="Body".plaintext()
1066 /// confirm="Yes"
1067 /// deny="No"
1068 /// />
1069 /// };
1070 ///
1071 /// let equivalent = Confirm::builder().title("Title")
1072 /// .text("Body".plaintext())
1073 /// .confirm("Yes")
1074 /// .deny("No")
1075 /// .build();
1076 ///
1077 /// assert_eq!(xml, equivalent);
1078 /// ```
1079 pub fn confirm() -> compose::confirm::build::ConfirmBuilderInit {
1080 compose::Confirm::builder()
1081 }
1082}