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}