Skip to content

Commit

Permalink
feat: text builder (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
cakekindel authored May 26, 2021
1 parent dea2269 commit 49e845a
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 4 deletions.
167 changes: 167 additions & 0 deletions src/compose/text/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ pub enum Text {
}

impl Text {
/// Build a new Text object
///
/// See TextBuilder for example
pub fn builder() -> build::TextBuilderInit {
build::TextBuilderInit::new()
}

/// Clone the data behind a reference, then convert it into
/// a `Text`
///
Expand All @@ -82,6 +89,166 @@ impl Text {
convert!(impl From<mrkdwn::Contents> for Text => |contents| Text::Mrkdwn(contents));
convert!(impl From<plain::Contents> for Text => |contents| Text::Plain(contents));

/// Text builder
pub mod build {
use std::marker::PhantomData;

use super::*;
use crate::build::*;

/// Builder methods
#[allow(non_camel_case_types)]
pub mod method {
/// TextBuilder.text
#[derive(Copy, Clone, Debug)]
pub struct text;

/// TextBuilder.plain or TextBuilder.mrkdwn
#[derive(Copy, Clone, Debug)]
pub struct plain_or_mrkdwn;
}

/// Initial state for Text Builder
pub type TextBuilderInit =
TextBuilder<Text, RequiredMethodNotCalled<method::text>>;

/// # Text Builder
///
/// Allows you to construct safely, with compile-time checks
/// on required setter methods.
///
/// # Required Methods
/// `TextBuilder::build()` is only available if these methods have been called:
/// - `text`
/// - `plain` or `mrkdwn`
///
/// ```
/// use slack_blocks::text::Text;
///
/// let foo = Text::builder().plain().text("foo").build();
/// ```
#[derive(Debug)]
pub struct TextBuilder<T, TMarker> {
text: Option<Text>,
text_value: Option<String>,
state: PhantomData<(T, TMarker)>,
}

impl<T> TextBuilder<T, RequiredMethodNotCalled<method::text>> {
/// Construct a new text builder
pub fn new() -> Self {
Self { text: None,
text_value: None,
state: PhantomData::<_> }
}

/// Set `text` (**Required**)
///
/// The text contents to render for this `Text` object.
///
/// For some basic formatting examples, see the docs for
/// the `text::Mrkdwn`, or [Slack's markdown docs 🔗].
///
/// There are no intrinsic length limits on this, those are usually
/// requirements of the context the text will be used in.
///
/// [Slack's markdown docs 🔗]: https://api.slack.com/reference/surfaces/formatting
pub fn text(mut self,
t: impl AsRef<str>)
-> TextBuilder<T, Set<method::text>> {
let text = t.as_ref().to_string();

match self.text {
| Some(Text::Mrkdwn(ref mut t)) => {
t.text = text;
},
| Some(Text::Plain(ref mut t)) => {
t.text = text;
},
| None => self.text_value = Some(text),
};

TextBuilder { text: self.text,
text_value: self.text_value,
state: PhantomData::<_> }
}
}

impl<M> TextBuilder<Text, M> {
/// Set the text you're building to be `plain_text` (**Required**)
pub fn plain(self) -> TextBuilder<Plain, M> {
let text = Some(Plain::from(self.text_value.unwrap_or_default()).into());
TextBuilder { text,
text_value: None,
state: PhantomData::<_> }
}

/// Set the text you're building to be `mrkdwn` (**Required**)
pub fn mrkdwn(self) -> TextBuilder<Mrkdwn, M> {
let text = Some(Mrkdwn::from(self.text_value.unwrap_or_default()).into());
TextBuilder { text,
text_value: None,
state: PhantomData::<_> }
}
}

impl<M> TextBuilder<Mrkdwn, M> {
/// Set `verbatim` (Optional)
///
/// When set to false (as is default)
/// URLs will be auto-converted into links,
/// conversation names will be link-ified,
/// and certain mentions will be automatically parsed.
///
/// Using a value of true will skip any preprocessing
/// of this nature, although you can
/// still include manual parsing strings.
pub fn verbatim(mut self) -> Self {
if let Some(Text::Mrkdwn(ref mut m)) = self.text {
m.verbatim = Some(true);
}

self
}
}

impl<M> TextBuilder<Plain, M> {
/// Set `emoji` (Optional)
///
/// Indicates whether emojis in a text field should be
/// escaped into the colon emoji format
pub fn emoji(mut self) -> Self {
if let Some(Text::Plain(ref mut p)) = self.text {
p.emoji = Some(true);
}

self
}
}

impl<T: Into<Text>> TextBuilder<T, Set<method::text>> {
/// All done building, now give me a darn text object!
///
/// > `no method name 'build' found for struct 'TextBuilder<...>'`?
/// Make sure all required setter methods have been called. See docs for `TextBuilder`.
///
/// ```compile_fail
/// use slack_blocks::text::Text;
///
/// let foo = Text::builder().build(); // Won't compile!
/// ```
///
/// ```
/// use slack_blocks::text::Text;
///
/// let foo = Text::builder().plain().emoji().text("foo :joy:").build();
/// ```
pub fn build(self) -> Text {
self.text.unwrap().into()
}
}
}

impl AsRef<str> for Text {
fn as_ref(&self) -> &str {
match self {
Expand Down
4 changes: 2 additions & 2 deletions src/compose/text/mrkdwn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ use serde::{Deserialize, Serialize};
/// </details>
#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize)]
pub struct Contents {
text: String,
pub(super) text: String,

#[serde(skip_serializing_if = "Option::is_none")]
verbatim: Option<bool>,
pub(super) verbatim: Option<bool>,
}

impl Contents {
Expand Down
4 changes: 2 additions & 2 deletions src/compose/text/plain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ use serde::{Deserialize, Serialize};
/// Only formatting available is emojis.
#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize)]
pub struct Contents {
text: String,
pub(super) text: String,

#[serde(skip_serializing_if = "Option::is_none")]
emoji: Option<bool>,
pub(super) emoji: Option<bool>,
}

impl Contents {
Expand Down

0 comments on commit 49e845a

Please sign in to comment.