slack_blocks/blocks/
section.rs

1//! # Section Block
2//!
3//! _[slack api docs 🔗]_
4//!
5//! Available in surfaces:
6//!  - [modals 🔗]
7//!  - [messages 🔗]
8//!  - [home tabs 🔗]
9//!
10//! A `section` is one of the most flexible blocks available -
11//! it can be used as a simple text block,
12//! in combination with text fields,
13//! or side-by-side with any of the available [block elements 🔗]
14//!
15//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#section
16//! [modals 🔗]: https://api.slack.com/surfaces/modals
17//! [messages 🔗]: https://api.slack.com/surfaces/messages
18//! [home tabs 🔗]: https://api.slack.com/surfaces/tabs
19//! [block elements 🔗]: https://api.slack.com/reference/messaging/block-elements
20
21use std::borrow::Cow;
22
23use serde::{Deserialize, Serialize};
24#[cfg(feature = "validation")]
25use validator::Validate;
26
27#[cfg(feature = "validation")]
28use crate::val_helpr::ValidationResult;
29use crate::{compose::text, elems::BlockElement};
30
31/// # Section Block
32///
33/// _[slack api docs 🔗]_
34///
35/// Available in surfaces:
36///  - [modals 🔗]
37///  - [messages 🔗]
38///  - [home tabs 🔗]
39///
40/// A `section` is one of the most flexible blocks available -
41/// it can be used as a simple text block,
42/// in combination with text fields,
43/// or side-by-side with any of the available [block elements 🔗]
44///
45/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#section
46/// [modals 🔗]: https://api.slack.com/surfaces/modals
47/// [messages 🔗]: https://api.slack.com/surfaces/messages
48/// [home tabs 🔗]: https://api.slack.com/surfaces/tabs
49/// [block elements 🔗]: https://api.slack.com/reference/messaging/block-elements
50#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
51#[cfg_attr(feature = "validation", derive(Validate))]
52pub struct Section<'a> {
53  #[serde(skip_serializing_if = "Option::is_none")]
54  #[cfg_attr(feature = "validation", validate(custom = "validate::fields"))]
55  fields: Option<Cow<'a, [text::Text]>>,
56
57  #[serde(skip_serializing_if = "Option::is_none")]
58  #[cfg_attr(feature = "validation", validate(custom = "validate::text"))]
59  text: Option<text::Text>,
60
61  #[serde(skip_serializing_if = "Option::is_none")]
62  #[cfg_attr(feature = "validation", validate(custom = "validate::block_id"))]
63  block_id: Option<Cow<'a, str>>,
64
65  /// One of the available [element objects 🔗][element_objects].
66  ///
67  /// [element_objects]: https://api.slack.com/reference/messaging/block-elements
68  #[serde(skip_serializing_if = "Option::is_none")]
69  accessory: Option<BlockElement<'a>>,
70}
71
72impl<'a> Section<'a> {
73  /// Build a new section block
74  ///
75  /// For example, see `blocks::section::build::SectionBuilder`.
76  pub fn builder() -> build::SectionBuilderInit<'a> {
77    build::SectionBuilderInit::new()
78  }
79
80  /// Validate that this Section block agrees with Slack's model requirements
81  ///
82  /// # Errors
83  /// - If `fields` contains more than 10 fields
84  /// - If one of `fields` longer than 2000 chars
85  /// - If `text` longer than 3000 chars
86  /// - If `block_id` longer than 255 chars
87  ///
88  /// # Example
89  /// ```
90  /// use slack_blocks::{blocks, compose::text};
91  ///
92  /// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
93  ///
94  /// let block = blocks::Section::builder().text(text::Plain::from("file_id"))
95  ///                                       .block_id(long_string)
96  ///                                       .build();
97  ///
98  /// assert_eq!(true, matches!(block.validate(), Err(_)));
99  /// ```
100  #[cfg(feature = "validation")]
101  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
102  pub fn validate(&self) -> ValidationResult {
103    Validate::validate(self)
104  }
105}
106
107/// Section block builder
108pub mod build {
109  use std::marker::PhantomData;
110
111  use super::*;
112  use crate::build::*;
113
114  /// Compile-time markers for builder methods
115  #[allow(non_camel_case_types)]
116  pub mod method {
117    /// SectionBuilder.text
118    #[derive(Clone, Copy, Debug)]
119    pub struct text;
120  }
121
122  /// Initial state for `SectionBuilder`
123  pub type SectionBuilderInit<'a> =
124    SectionBuilder<'a, RequiredMethodNotCalled<method::text>>;
125
126  /// Build an Section block
127  ///
128  /// Allows you to construct safely, with compile-time checks
129  /// on required setter methods.
130  ///
131  /// # Required Methods
132  /// `SectionBuilder::build()` is only available if these methods have been called:
133  ///  - `text` **or** `field(s)`, both may be called.
134  ///
135  /// # Example
136  /// ```
137  /// use slack_blocks::{blocks::Section,
138  ///                    elems::Image,
139  ///                    text,
140  ///                    text::ToSlackPlaintext};
141  ///
142  /// let block =
143  ///   Section::builder().text("foo".plaintext())
144  ///                     .field("bar".plaintext())
145  ///                     .field("baz".plaintext())
146  ///                     // alternatively:
147  ///                     .fields(vec!["bar".plaintext(),
148  ///                                  "baz".plaintext()]
149  ///                                  .into_iter()
150  ///                                  .map(text::Text::from)
151  ///                     )
152  ///                     .accessory(Image::builder().image_url("foo.png")
153  ///                                                .alt_text("pic of foo")
154  ///                                                .build())
155  ///                     .build();
156  /// ```
157  #[derive(Debug)]
158  pub struct SectionBuilder<'a, Text> {
159    accessory: Option<BlockElement<'a>>,
160    text: Option<text::Text>,
161    fields: Option<Vec<text::Text>>,
162    block_id: Option<Cow<'a, str>>,
163    state: PhantomData<Text>,
164  }
165
166  impl<'a, E> SectionBuilder<'a, E> {
167    /// Create a new SectionBuilder
168    pub fn new() -> Self {
169      Self { accessory: None,
170             text: None,
171             fields: None,
172             block_id: None,
173             state: PhantomData::<_> }
174    }
175
176    /// Set `accessory` (Optional)
177    pub fn accessory<B>(mut self, acc: B) -> Self
178      where B: Into<BlockElement<'a>>
179    {
180      self.accessory = Some(acc.into());
181      self
182    }
183
184    /// Add `text` (**Required: this or `field(s)`**)
185    ///
186    /// The text for the block, in the form of a [text object 🔗].
187    ///
188    /// Maximum length for the text in this field is 3000 characters.
189    ///
190    /// [text object 🔗]: https://api.slack.com/reference/messaging/composition-objects#text
191    pub fn text<T>(self, text: T) -> SectionBuilder<'a, Set<method::text>>
192      where T: Into<text::Text>
193    {
194      SectionBuilder { accessory: self.accessory,
195                       text: Some(text.into()),
196                       fields: self.fields,
197                       block_id: self.block_id,
198                       state: PhantomData::<_> }
199    }
200
201    /// Set `fields` (**Required: this or `text`**)
202    ///
203    /// A collection of [text objects 🔗].
204    ///
205    /// Any text objects included with fields will be
206    /// rendered in a compact format that allows for
207    /// 2 columns of side-by-side text.
208    ///
209    /// Maximum number of items is 10.
210    ///
211    /// Maximum length for the text in each item is 2000 characters.
212    ///
213    /// [text objects 🔗]: https://api.slack.com/reference/messaging/composition-objects#text
214    pub fn fields<I>(self, fields: I) -> SectionBuilder<'a, Set<method::text>>
215      where I: IntoIterator<Item = text::Text>
216    {
217      SectionBuilder { accessory: self.accessory,
218                       text: self.text,
219                       fields: Some(fields.into_iter().collect()),
220                       block_id: self.block_id,
221                       state: PhantomData::<_> }
222    }
223
224    /// Append a single field to `fields`.
225    pub fn field<T>(mut self, text: T) -> SectionBuilder<'a, Set<method::text>>
226      where T: Into<text::Text>
227    {
228      let mut fields = self.fields.take().unwrap_or_default();
229      fields.push(text.into());
230
231      self.fields(fields)
232    }
233
234    /// XML macro children, appends `fields` to the Section.
235    ///
236    /// To set `text`, use the `text` attribute.
237    /// ```
238    /// use slack_blocks::{blocks::Section, blox::*, text, text::ToSlackPlaintext};
239    ///
240    /// let xml = blox! {
241    ///   <section_block text={"Section".plaintext()}>
242    ///     <text kind=plain>"Foo"</text>
243    ///     <text kind=plain>"Bar"</text>
244    ///   </section_block>
245    /// };
246    ///
247    /// let equiv = Section::builder().text("Section".plaintext())
248    ///                               .field("Foo".plaintext())
249    ///                               .field("Bar".plaintext())
250    ///                               .build();
251    ///
252    /// assert_eq!(xml, equiv);
253    /// ```
254    #[cfg(feature = "blox")]
255    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
256    pub fn child<T>(self, text: T) -> SectionBuilder<'a, Set<method::text>>
257      where T: Into<text::Text>
258    {
259      self.field(text)
260    }
261
262    /// Set `block_id` (Optional)
263    ///
264    /// A string acting as a unique identifier for a block.
265    ///
266    /// You can use this `block_id` when you receive an interaction payload
267    /// to [identify the source of the action 🔗].
268    ///
269    /// If not specified, a `block_id` will be generated.
270    ///
271    /// Maximum length for this field is 255 characters.
272    ///
273    /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
274    pub fn block_id<S>(mut self, block_id: S) -> Self
275      where S: Into<Cow<'a, str>>
276    {
277      self.block_id = Some(block_id.into());
278      self
279    }
280  }
281
282  impl<'a> SectionBuilder<'a, Set<method::text>> {
283    /// All done building, now give me a darn actions block!
284    ///
285    /// > `no method name 'build' found for struct 'SectionBuilder<...>'`?
286    /// Make sure all required setter methods have been called. See docs for `SectionBuilder`.
287    ///
288    /// ```compile_fail
289    /// use slack_blocks::blocks::Section;
290    ///
291    /// let foo = Section::builder().build(); // Won't compile!
292    /// ```
293    ///
294    /// ```
295    /// use slack_blocks::{blocks::Section,
296    ///                    compose::text::ToSlackPlaintext,
297    ///                    elems::Image};
298    ///
299    /// let block =
300    ///   Section::builder().text("foo".plaintext())
301    ///                     .accessory(Image::builder().image_url("foo.png")
302    ///                                                .alt_text("pic of foo")
303    ///                                                .build())
304    ///                     .build();
305    /// ```
306    pub fn build(self) -> Section<'a> {
307      Section { text: self.text,
308                fields: self.fields.map(|fs| fs.into()),
309                accessory: self.accessory,
310                block_id: self.block_id }
311    }
312  }
313}
314
315#[cfg(feature = "validation")]
316mod validate {
317  use super::*;
318  use crate::{compose::text,
319              val_helpr::{below_len, ValidatorResult}};
320
321  pub(super) fn text(text: &text::Text) -> ValidatorResult {
322    below_len("Section.text", 3000, text.as_ref())
323  }
324
325  pub(super) fn block_id(text: &Cow<str>) -> ValidatorResult {
326    below_len("Section.block_id", 255, text.as_ref())
327  }
328
329  pub(super) fn fields(texts: &Cow<[text::Text]>) -> ValidatorResult {
330    below_len("Section.fields", 10, texts.as_ref()).and(
331                                                        texts.iter()
332                                                             .map(|text| {
333                                                               below_len(
334             "Section.fields",
335             2000,
336             text.as_ref())
337                                                             })
338                                                             .collect(),
339    )
340  }
341}