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());