slack_blocks/blocks/context.rs
1//! # Context Block
2//!
3//! _[slack api docs 🔗][context_docs]_
4//!
5//! Displays message context, which can include both images and text.
6//!
7//! [context_docs]: https://api.slack.com/reference/block-kit/blocks#context
8
9use std::borrow::Cow;
10
11use serde::{Deserialize, Serialize};
12#[cfg(feature = "validation")]
13use validator::Validate;
14
15#[cfg(feature = "validation")]
16use crate::val_helpr::ValidationResult;
17use crate::{convert,
18 elems::{BlockElement, Image},
19 text};
20
21/// # Context Block
22///
23/// _[slack api docs 🔗][context_docs]_
24///
25/// Displays message context, which can include both images and text.
26///
27/// [context_docs]: https://api.slack.com/reference/block-kit/blocks#context
28#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize)]
29#[cfg_attr(feature = "validation", derive(Validate))]
30pub struct Context<'a> {
31 #[cfg_attr(feature = "validation", validate(length(max = 10)))]
32 elements: Vec<ImageOrText<'a>>,
33
34 #[serde(skip_serializing_if = "Option::is_none")]
35 #[cfg_attr(feature = "validation",
36 validate(custom = "super::validate_block_id"))]
37 block_id: Option<Cow<'a, str>>,
38}
39
40impl<'a> Context<'a> {
41 /// Build a new Context block.
42 ///
43 /// For example, see docs for ContextBuilder.
44 pub fn builder() -> build::ContextBuilderInit<'a> {
45 build::ContextBuilderInit::new()
46 }
47
48 /// Validate that this Context block agrees with Slack's model requirements
49 ///
50 /// # Errors
51 /// - If `block_id` longer than 255 chars
52 /// - If `elements` contains more than 10 objects
53 ///
54 /// # Example
55 /// ```
56 /// use slack_blocks::{blocks::Context, text::ToSlackPlaintext};
57 ///
58 /// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
59 ///
60 /// let block = Context::builder().element("foo".plaintext())
61 /// .block_id(long_string)
62 /// .build();
63 ///
64 /// assert_eq!(true, matches!(block.validate(), Err(_)));
65 /// ```
66 #[cfg(feature = "validation")]
67 #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
68 pub fn validate(&self) -> ValidationResult {
69 Validate::validate(self)
70 }
71}
72
73/// Context block builder
74pub mod build {
75 use std::marker::PhantomData;
76
77 use super::*;
78 use crate::build::*;
79
80 /// Compile-time markers for builder methods
81 #[allow(non_camel_case_types)]
82 pub mod method {
83 /// ContextBuilder.elements
84 #[derive(Clone, Copy, Debug)]
85 pub struct elements;
86 }
87
88 /// Initial state for `ContextBuilder`
89 pub type ContextBuilderInit<'a> =
90 ContextBuilder<'a, RequiredMethodNotCalled<method::elements>>;
91
92 /// Build an Context block
93 ///
94 /// Allows you to construct safely, with compile-time checks
95 /// on required setter methods.
96 ///
97 /// # Required Methods
98 /// `ContextBuilder::build()` is only available if these methods have been called:
99 /// - `element`
100 ///
101 /// # Example
102 /// ```
103 /// use slack_blocks::{blocks::Context, elems::Image, text::ToSlackPlaintext};
104 ///
105 /// let block = Context::builder().element("foo".plaintext())
106 /// .element(Image::builder().image_url("foo.png")
107 /// .alt_text("pic of foo")
108 /// .build())
109 /// .build();
110 /// ```
111 #[derive(Debug)]
112 pub struct ContextBuilder<'a, Elements> {
113 elements: Option<Vec<ImageOrText<'a>>>,
114 block_id: Option<Cow<'a, str>>,
115 state: PhantomData<Elements>,
116 }
117
118 impl<'a, E> ContextBuilder<'a, E> {
119 /// Create a new ContextBuilder
120 pub fn new() -> Self {
121 Self { elements: None,
122 block_id: None,
123 state: PhantomData::<_> }
124 }
125
126 /// Alias of `element` for appending an element with an XML child.
127 #[cfg(feature = "blox")]
128 #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
129 pub fn child<El>(self,
130 element: El)
131 -> ContextBuilder<'a, Set<method::elements>>
132 where El: Into<ImageOrText<'a>>
133 {
134 self.element(element)
135 }
136
137 /// Add an `element` (**Required**, can be called many times)
138 ///
139 /// A composition object; Must be image elements or text objects.
140 ///
141 /// Maximum number of items is 10.
142 pub fn element<El>(self,
143 element: El)
144 -> ContextBuilder<'a, Set<method::elements>>
145 where El: Into<ImageOrText<'a>>
146 {
147 let mut elements = self.elements.unwrap_or_default();
148 elements.push(element.into());
149
150 ContextBuilder { block_id: self.block_id,
151 elements: Some(elements),
152 state: PhantomData::<_> }
153 }
154
155 /// Set `block_id` (Optional)
156 ///
157 /// A string acting as a unique identifier for a block.
158 ///
159 /// You can use this `block_id` when you receive an interaction payload
160 /// to [identify the source of the action 🔗].
161 ///
162 /// If not specified, a `block_id` will be generated.
163 ///
164 /// Maximum length for this field is 255 characters.
165 ///
166 /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
167 pub fn block_id<S>(mut self, block_id: S) -> Self
168 where S: Into<Cow<'a, str>>
169 {
170 self.block_id = Some(block_id.into());
171 self
172 }
173 }
174
175 impl<'a> ContextBuilder<'a, Set<method::elements>> {
176 /// All done building, now give me a darn actions block!
177 ///
178 /// > `no method name 'build' found for struct 'ContextBuilder<...>'`?
179 /// Make sure all required setter methods have been called. See docs for `ContextBuilder`.
180 ///
181 /// ```compile_fail
182 /// use slack_blocks::blocks::Context;
183 ///
184 /// let foo = Context::builder().build(); // Won't compile!
185 /// ```
186 ///
187 /// ```
188 /// use slack_blocks::{blocks::Context,
189 /// compose::text::ToSlackPlaintext,
190 /// elems::Image};
191 ///
192 /// let block = Context::builder().element("foo".plaintext())
193 /// .element(Image::builder().image_url("foo.png")
194 /// .alt_text("pic of foo")
195 /// .build())
196 /// .build();
197 /// ```
198 pub fn build(self) -> Context<'a> {
199 Context { elements: self.elements.unwrap(),
200 block_id: self.block_id }
201 }
202 }
203}
204
205impl<'a> From<Vec<ImageOrText<'a>>> for Context<'a> {
206 fn from(elements: Vec<ImageOrText<'a>>) -> Self {
207 Self { elements,
208 ..Default::default() }
209 }
210}
211
212/// The Composition objects supported by this block
213#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
214#[allow(missing_docs)]
215#[serde(untagged)]
216pub enum ImageOrText<'a> {
217 Text(text::Text),
218 Image(BlockElement<'a>),
219}
220
221convert!(impl From<text::Text> for ImageOrText<'static> => |txt| ImageOrText::Text(txt));
222convert!(impl<'a> From<Image<'a>> for ImageOrText<'a> => |i| ImageOrText::Image(BlockElement::from(i)));
223convert!(impl From<text::Plain> for ImageOrText<'static> => |t| text::Text::from(t).into());
224convert!(impl From<text::Mrkdwn> for ImageOrText<'static> => |t| text::Text::from(t).into());