slack_blocks/blocks/
image.rs

1//! # Image Block
2//!
3//! _[slack api docs 🔗]_
4//!
5//! A simple image block, designed to make those cat photos really pop.
6//!
7//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#image
8
9use std::borrow::Cow;
10
11use serde::{Deserialize, Serialize};
12#[cfg(feature = "validation")]
13use validator::Validate;
14
15use crate::compose::text;
16#[cfg(feature = "validation")]
17use crate::val_helpr::ValidationResult;
18
19/// # Image Block
20///
21/// _[slack api docs 🔗]_
22///
23/// A simple image block, designed to make those cat photos really pop.
24///
25/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#image
26#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize)]
27#[cfg_attr(feature = "validation", derive(Validate))]
28pub struct Image<'a> {
29  #[cfg_attr(feature = "validation", validate(length(max = 3000)))]
30  image_url: Cow<'a, str>,
31
32  #[cfg_attr(feature = "validation", validate(length(max = 2000)))]
33  alt_text: Cow<'a, str>,
34
35  #[serde(skip_serializing_if = "Option::is_none")]
36  #[cfg_attr(feature = "validation", validate(custom = "validate::title"))]
37  title: Option<text::Text>,
38
39  #[serde(skip_serializing_if = "Option::is_none")]
40  #[cfg_attr(feature = "validation",
41             validate(custom = "super::validate_block_id"))]
42  block_id: Option<Cow<'a, str>>,
43}
44
45impl<'a> Image<'a> {
46  /// Build a new Image block.
47  ///
48  /// For example, see docs for ImageBuilder.
49  pub fn builder() -> build::ImageBuilderInit<'a> {
50    build::ImageBuilderInit::new()
51  }
52
53  /// Validate that this Image block agrees with Slack's model requirements
54  ///
55  /// # Errors
56  /// - If `block_id` longer
57  ///     than 255 chars
58  /// - If title longer than 2000 chars
59  /// - If `alt_text` longer than 2000 chars
60  /// - If `image_url` longer than 3000 chars
61  ///
62  /// # Example
63  /// ```
64  /// use slack_blocks::blocks;
65  ///
66  /// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
67  ///
68  /// let block = blocks::Image::builder().image_url("")
69  ///                                     .alt("")
70  ///                                     .block_id(long_string)
71  ///                                     .build();
72  ///
73  /// assert_eq!(true, matches!(block.validate(), Err(_)));
74  /// ```
75  #[cfg(feature = "validation")]
76  #[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
77  pub fn validate(&self) -> ValidationResult {
78    Validate::validate(self)
79  }
80}
81
82/// File block builder
83pub mod build {
84  use std::marker::PhantomData;
85
86  use super::*;
87  use crate::build::*;
88
89  /// Compile-time markers for builder methods
90  #[allow(non_camel_case_types)]
91  pub mod method {
92    /// ImageBuilder.image_url or src
93    #[derive(Clone, Copy, Debug)]
94    pub struct url;
95
96    /// ImageBuilder.alt_text or alt
97    #[derive(Clone, Copy, Debug)]
98    pub struct alt;
99  }
100
101  /// Initial state for `ImageBuilder`
102  pub type ImageBuilderInit<'a> =
103    ImageBuilder<'a,
104                 RequiredMethodNotCalled<method::url>,
105                 RequiredMethodNotCalled<method::alt>>;
106
107  /// Build an Image block
108  ///
109  /// Allows you to construct safely, with compile-time checks
110  /// on required setter methods.
111  ///
112  /// # Required Methods
113  /// `ImageBuilder::build()` is only available if these methods have been called:
114  ///  - `external_id`
115  ///  - `source`
116  ///
117  /// # Example
118  /// ```
119  /// use slack_blocks::{blocks::Image, text::ToSlackPlaintext};
120  ///
121  /// let block = Image::builder().image_url("https://foo.com/bar.png")
122  ///                             .alt_text("pic of bar")
123  ///                             .build();
124  /// ```
125  #[derive(Debug)]
126  pub struct ImageBuilder<'a, Url, Alt> {
127    image_url: Option<Cow<'a, str>>,
128    alt_text: Option<Cow<'a, str>>,
129    title: Option<text::Text>,
130    block_id: Option<Cow<'a, str>>,
131    state: PhantomData<(Url, Alt)>,
132  }
133
134  impl<'a, Url, Alt> ImageBuilder<'a, Url, Alt> {
135    /// Create a new ImageBuilder
136    pub fn new() -> Self {
137      Self { image_url: None,
138             alt_text: None,
139             title: None,
140             block_id: None,
141             state: PhantomData::<_> }
142    }
143
144    /// Set `title` (Optional)
145    ///
146    /// An optional title for the image in the form of a
147    /// Plaintext [text object 🔗].
148    ///
149    /// Maximum length for the text in this field is 2000 characters.
150    ///
151    /// [text object 🔗]: https://api.slack.com/reference/messaging/composition-objects#text
152    pub fn title<T>(mut self, text: T) -> Self
153      where T: Into<text::Plain>
154    {
155      self.title = Some(text.into().into());
156      self
157    }
158
159    /// Alias for `image_url`.
160    pub fn src<S>(self, image_url: S) -> ImageBuilder<'a, Set<method::url>, Alt>
161      where S: Into<Cow<'a, str>>
162    {
163      self.image_url(image_url)
164    }
165
166    /// Set `image_url` (**Required**)
167    ///
168    /// The URL of the image to be displayed.
169    ///
170    /// Maximum length for this field is 3000 characters.
171    pub fn image_url<S>(self,
172                        image_url: S)
173                        -> ImageBuilder<'a, Set<method::url>, Alt>
174      where S: Into<Cow<'a, str>>
175    {
176      ImageBuilder { image_url: Some(image_url.into()),
177                     alt_text: self.alt_text,
178                     title: self.title,
179                     block_id: self.block_id,
180                     state: PhantomData::<_> }
181    }
182
183    /// Set `alt_text` (**Required**)
184    ///
185    /// A plain-text summary of the image.
186    ///
187    /// This should not contain any markup.
188    ///
189    /// Maximum length for this field is 2000 characters.
190    pub fn alt_text<S>(self,
191                       alt_text: S)
192                       -> ImageBuilder<'a, Url, Set<method::alt>>
193      where S: Into<Cow<'a, str>>
194    {
195      ImageBuilder { alt_text: Some(alt_text.into()),
196                     image_url: self.image_url,
197                     title: self.title,
198                     block_id: self.block_id,
199                     state: PhantomData::<_> }
200    }
201
202    /// Alias for `alt_text`.
203    pub fn alt<S>(self, alt_text: S) -> ImageBuilder<'a, Url, Set<method::alt>>
204      where S: Into<Cow<'a, str>>
205    {
206      self.alt_text(alt_text)
207    }
208
209    /// Set `block_id` (Optional)
210    ///
211    /// A string acting as a unique identifier for a block.
212    ///
213    /// You can use this `block_id` when you receive an interaction payload
214    /// to [identify the source of the action 🔗].
215    ///
216    /// If not specified, a `block_id` will be generated.
217    ///
218    /// Maximum length for this field is 255 characters.
219    ///
220    /// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
221    pub fn block_id<S>(mut self, block_id: S) -> Self
222      where S: Into<Cow<'a, str>>
223    {
224      self.block_id = Some(block_id.into());
225      self
226    }
227  }
228
229  impl<'a> ImageBuilder<'a, Set<method::url>, Set<method::alt>> {
230    /// All done building, now give me a darn actions block!
231    ///
232    /// > `no method name 'build' found for struct 'ImageBuilder<...>'`?
233    /// Make sure all required setter methods have been called. See docs for `ImageBuilder`.
234    ///
235    /// ```compile_fail
236    /// use slack_blocks::blocks::Image;
237    ///
238    /// let foo = Image::builder().build(); // Won't compile!
239    /// ```
240    ///
241    /// ```
242    /// use slack_blocks::{blocks::Image, compose::text::ToSlackPlaintext};
243    ///
244    /// let block = Image::builder().image_url("https://foo.com/bar.png")
245    ///                             .alt_text("pic of bar")
246    ///                             .build();
247    /// ```
248    pub fn build(self) -> Image<'a> {
249      Image { image_url: self.image_url.unwrap(),
250              alt_text: self.alt_text.unwrap(),
251              title: self.title,
252              block_id: self.block_id }
253    }
254  }
255}
256
257#[cfg(feature = "validation")]
258mod validate {
259  use crate::{compose::text,
260              val_helpr::{below_len, ValidatorResult}};
261
262  pub(super) fn title(text: &text::Text) -> ValidatorResult {
263    below_len("Image Title", 2000, text.as_ref())
264  }
265}