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}