slack_blocks/compose/text/
mod.rs

1//! # Text Object
2//! [_slack api docs 🔗_](https://api.slack.com/reference/block-kit/composition-objects#text)
3//!
4//! An object containing some text,
5//! formatted either as `plain_text`
6//! or using [`mrkdwn` 🔗](https://api.slack.com/reference/surfaces/formatting),
7//! our proprietary textual markup that's just different enough
8//! from Markdown to frustrate you.
9
10use serde::{Deserialize, Serialize};
11
12use crate::convert;
13
14pub mod mrkdwn;
15pub mod plain;
16
17#[doc(inline)]
18pub use mrkdwn::Contents as Mrkdwn;
19#[doc(inline)]
20pub use plain::Contents as Plain;
21
22/// Convenience trait to provide a little more meaning than
23/// a call to `"foo".into()`, and shorter than `text::Plain::from("foo")`
24pub trait ToSlackPlaintext: Sized + Into<Plain> {
25  /// Convert to slack plain_text
26  fn plaintext(self) -> Plain {
27    self.into()
28  }
29}
30
31impl<T: Into<Plain>> ToSlackPlaintext for T {}
32
33/// Convenience trait to provide a little more meaning than
34/// a call to `"foo".into()`, and shorter than `text::Mrkdwn::from("foo")`
35pub trait ToSlackMarkdown: Sized + Into<Mrkdwn> {
36  /// Convert to slack plain_text
37  fn markdown(self) -> Mrkdwn {
38    self.into()
39  }
40}
41
42impl<T: Into<Mrkdwn>> ToSlackMarkdown for T {}
43
44/// # Text Object
45/// [_slack api docs 🔗_](https://api.slack.com/reference/block-kit/composition-objects#text)
46///
47/// An object containing some text,
48/// formatted either as `plain_text`
49/// or using [`mrkdwn` 🔗](https://api.slack.com/reference/surfaces/formatting),
50/// our proprietary textual markup that's just different enough
51/// from Markdown to frustrate you.
52#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
53#[serde(tag = "type", rename_all = "snake_case")]
54pub enum Text {
55  /// Markdown text
56  Mrkdwn(mrkdwn::Contents),
57  /// Plain text
58  #[serde(rename = "plain_text")]
59  Plain(plain::Contents),
60}
61
62impl Text {
63  /// Build a new Text object
64  ///
65  /// See TextBuilder for example
66  pub fn builder() -> build::TextBuilderInit {
67    build::TextBuilderInit::new()
68  }
69
70  /// Clone the data behind a reference, then convert it into
71  /// a `Text`
72  ///
73  /// # Arguments
74  /// - `contents` - Anything that can be cloned into a type
75  ///     that is convertable to a `Text` - this includes
76  ///     the `Plain` and `Mrkdwn` contents structs.
77  ///     Notably, this doesn't include a conversion directly
78  ///     from a reference to a `String` or a `&str` - that's
79  ///     because assuming which kind of text a string represents
80  ///     could lead to unexpected behavior when that kind of text
81  ///     isn't valid.
82  pub fn copy_from<T: Into<Self> + Clone>(contents: &T) -> Self {
83    contents.clone().into()
84  }
85}
86
87convert!(impl From<mrkdwn::Contents> for Text => |contents| Text::Mrkdwn(contents));
88convert!(impl From<plain::Contents> for Text => |contents| Text::Plain(contents));
89
90/// Text builder
91pub mod build {
92  use std::marker::PhantomData;
93
94  use super::*;
95  use crate::build::*;
96
97  /// Builder methods
98  #[allow(non_camel_case_types)]
99  pub mod method {
100    /// TextBuilder.text
101    #[derive(Copy, Clone, Debug)]
102    pub struct text;
103
104    /// TextBuilder.plain or TextBuilder.mrkdwn
105    #[derive(Copy, Clone, Debug)]
106    pub struct plain_or_mrkdwn;
107  }
108
109  /// "Text Kind" for XML macro
110  #[allow(non_camel_case_types)]
111  #[cfg(feature = "blox")]
112  #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
113  pub mod kind {
114    use super::*;
115    // This trick is kind of nasty. Essentially, I want the API to look like
116    // `<text plain>"Foo"</text>`, not `<text_plain>"Foo"</text_plain>`,
117    // with an attribute setting the text kind.
118    //
119    // mox does not support nullary function attributes, so I _could_ make
120    // `TextBuilder.plain()` and `TextBuilder.mrkdwn()` accept a unit argument,
121    // but that would look fugly and impact non-XML use-cases.
122    // (`<text plain=()>"Foo"</text>`, `Text::builder().plain(()).text("Foo").build()`)
123    //
124    // So I settled on having types named `mrkdwn` and `plain` exported from `crate::mox`
125    // and a `TextMethod.kind()` method -
126    // (`<text kind=mrkdwn></text>`)
127    // this feels good to write, looks good to read, the only cons are implementation complexity
128    // and potential clash with locals named `mrkdwn` or `plain`.
129    /// Static marker trait tying the token "mrkdwn" to the type "Mrkdwn",
130    /// and "plain" to "Plain"
131    pub trait TextKind<T> {
132      /// Invoke `TextBuilder.mrkdwn` or `TextBuilder.plain`
133      fn set_kind<M>(self, builder: TextBuilder<Text, M>) -> TextBuilder<T, M>;
134    }
135
136    /// Markdown text
137    #[derive(Copy, Clone, Debug)]
138    pub struct mrkdwn;
139
140    /// Plain text
141    #[derive(Copy, Clone, Debug)]
142    pub struct plain;
143
144    impl TextKind<Mrkdwn> for mrkdwn {
145      fn set_kind<M>(self,
146                     builder: TextBuilder<Text, M>)
147                     -> TextBuilder<Mrkdwn, M> {
148        builder.mrkdwn()
149      }
150    }
151
152    impl TextKind<Plain> for plain {
153      fn set_kind<M>(self,
154                     builder: TextBuilder<Text, M>)
155                     -> TextBuilder<Plain, M> {
156        builder.plain()
157      }
158    }
159  }
160
161  /// Initial state for Text Builder
162  pub type TextBuilderInit =
163    TextBuilder<Text, RequiredMethodNotCalled<method::text>>;
164
165  /// # Text Builder
166  ///
167  /// Allows you to construct safely, with compile-time checks
168  /// on required setter methods.
169  ///
170  /// # Required Methods
171  /// `TextBuilder::build()` is only available if these methods have been called:
172  ///  - `text`
173  ///  - `plain` or `mrkdwn`
174  ///
175  /// ```
176  /// use slack_blocks::text::Text;
177  ///
178  /// let foo = Text::builder().plain().text("foo").build();
179  /// ```
180  #[derive(Debug)]
181  pub struct TextBuilder<T, TMarker> {
182    text: Option<Text>,
183    text_value: Option<String>,
184    state: PhantomData<(T, TMarker)>,
185  }
186
187  impl<T> TextBuilder<T, RequiredMethodNotCalled<method::text>> {
188    /// Construct a new text builder
189    pub fn new() -> Self {
190      Self { text: None,
191             text_value: None,
192             state: PhantomData::<_> }
193    }
194
195    /// Set `text` (**Required**)
196    ///
197    /// The text contents to render for this `Text` object.
198    ///
199    /// For some basic formatting examples, see the docs for
200    /// the `text::Mrkdwn`, or [Slack's markdown docs 🔗].
201    ///
202    /// There are no intrinsic length limits on this, those are usually
203    /// requirements of the context the text will be used in.
204    ///
205    /// [Slack's markdown docs 🔗]: https://api.slack.com/reference/surfaces/formatting
206    pub fn text(mut self,
207                t: impl AsRef<str>)
208                -> TextBuilder<T, Set<method::text>> {
209      let text = t.as_ref().to_string();
210
211      match self.text {
212        | Some(Text::Mrkdwn(ref mut t)) => {
213          t.text = text;
214        },
215        | Some(Text::Plain(ref mut t)) => {
216          t.text = text;
217        },
218        | None => self.text_value = Some(text),
219      };
220
221      TextBuilder { text: self.text,
222                    text_value: self.text_value,
223                    state: PhantomData::<_> }
224    }
225
226    /// Alias of `text` for XML macros, allowing text
227    /// to be set as a string literal instead of an attribute.
228    ///
229    /// ```
230    /// use slack_blocks::blox::*;
231    ///
232    /// let as_attr = blox! {
233    ///   <text kind=plain text="Foo" />
234    /// };
235    ///
236    /// let as_child = blox! {
237    ///   <text kind=plain>"Foo"</text>
238    /// };
239    ///
240    /// assert_eq!(as_attr, as_child);
241    /// ```
242    #[cfg(feature = "blox")]
243    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
244    pub fn child(self,
245                 t: impl AsRef<str>)
246                 -> TextBuilder<T, Set<method::text>> {
247      self.text(t)
248    }
249  }
250
251  impl<M> TextBuilder<Text, M> {
252    /// Set the kind of the text you're building (**Required**)
253    ///
254    /// Intended to be used as an XML attribute with `build::kind::mrkdwn` or `build::kind::plain`
255    ///
256    /// ```
257    /// use slack_blocks::{blox::*, text};
258    ///
259    /// let xml = blox! {<text kind=plain>"Foo"</text>};
260    ///
261    /// let builder = text::Plain::from("Foo");
262    ///
263    /// assert_eq!(xml, builder)
264    /// ```
265    #[cfg(feature = "blox")]
266    #[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
267    pub fn kind<T, K>(self, kind: K) -> TextBuilder<T, M>
268      where T: Into<Text>,
269            K: kind::TextKind<T>
270    {
271      kind.set_kind(self)
272    }
273
274    /// Set the text you're building to be `plain_text` (**Required**)
275    pub fn plain(self) -> TextBuilder<Plain, M> {
276      let text = Some(Plain::from(self.text_value.unwrap_or_default()).into());
277      TextBuilder { text,
278                    text_value: None,
279                    state: PhantomData::<_> }
280    }
281
282    /// Set the text you're building to be `mrkdwn` (**Required**)
283    pub fn mrkdwn(self) -> TextBuilder<Mrkdwn, M> {
284      let text = Some(Mrkdwn::from(self.text_value.unwrap_or_default()).into());
285      TextBuilder { text,
286                    text_value: None,
287                    state: PhantomData::<_> }
288    }
289  }
290
291  impl<M> TextBuilder<Mrkdwn, M> {
292    /// Set `verbatim` (Optional)
293    ///
294    /// When set to false (as is default)
295    /// URLs will be auto-converted into links,
296    /// conversation names will be link-ified,
297    /// and certain mentions will be automatically parsed.
298    ///
299    /// Using a value of true will skip any preprocessing
300    /// of this nature, although you can
301    /// still include manual parsing strings.
302    pub fn verbatim(mut self, verbatim: bool) -> Self {
303      if let Some(Text::Mrkdwn(ref mut m)) = self.text {
304        m.verbatim = Some(verbatim);
305      }
306
307      self
308    }
309  }
310
311  impl<M> TextBuilder<Plain, M> {
312    /// Set `emoji` (Optional)
313    ///
314    /// Indicates whether emojis in a text field should be
315    /// escaped into the colon emoji format
316    pub fn emoji(mut self, emoji: bool) -> Self {
317      if let Some(Text::Plain(ref mut p)) = self.text {
318        p.emoji = Some(emoji);
319      }
320
321      self
322    }
323  }
324
325  impl TextBuilder<Plain, Set<method::text>> {
326    /// All done building, now give me a darn text object!
327    ///
328    /// > `no method name 'build' found for struct 'TextBuilder<...>'`?
329    /// Make sure all required setter methods have been called. See docs for `TextBuilder`.
330    ///
331    /// ```compile_fail
332    /// use slack_blocks::text::Text;
333    ///
334    /// let foo = Text::builder().build(); // Won't compile!
335    /// ```
336    ///
337    /// ```
338    /// use slack_blocks::text::Text;
339    ///
340    /// let foo = Text::builder().plain()
341    ///                          .emoji(true)
342    ///                          .text("foo :joy:")
343    ///                          .build();
344    /// ```
345    pub fn build(self) -> Plain {
346      match self.text.unwrap() {
347        | Text::Plain(p) => p,
348        | _ => unreachable!("type marker says this should be plain."),
349      }
350    }
351  }
352
353  impl TextBuilder<Mrkdwn, Set<method::text>> {
354    /// All done building, now give me a darn text object!
355    ///
356    /// > `no method name 'build' found for struct 'TextBuilder<...>'`?
357    /// Make sure all required setter methods have been called. See docs for `TextBuilder`.
358    ///
359    /// ```compile_fail
360    /// use slack_blocks::text::Text;
361    ///
362    /// let foo = Text::builder().build(); // Won't compile!
363    /// ```
364    ///
365    /// ```
366    /// use slack_blocks::text::Text;
367    ///
368    /// let foo = Text::builder().mrkdwn()
369    ///                          .verbatim(true)
370    ///                          .text("foo :joy:")
371    ///                          .build();
372    /// ```
373    pub fn build(self) -> Mrkdwn {
374      match self.text.unwrap() {
375        | Text::Mrkdwn(p) => p,
376        | _ => unreachable!("type marker says this should be markdown."),
377      }
378    }
379  }
380}
381
382impl AsRef<str> for Text {
383  fn as_ref(&self) -> &str {
384    match self {
385      | Self::Mrkdwn(cts) => cts.as_ref(),
386      | Self::Plain(cts) => cts.as_ref(),
387    }
388  }
389}