Skip to content

Commit

Permalink
feat: context builder (#147)
Browse files Browse the repository at this point in the history
* feat: context builderd

* deprecate
  • Loading branch information
cakekindel authored May 25, 2021
1 parent 9ac0119 commit da4d125
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 24 deletions.
6 changes: 1 addition & 5 deletions src/blocks/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,10 @@ pub struct Actions<'a> {
elements: Vec<SupportedElement<'a>>,

#[serde(skip_serializing_if = "Option::is_none")]
#[validate(custom = "validate_block_id")]
#[validate(custom = "super::validate_block_id")]
block_id: Option<Cow<'a, str>>,
}

fn validate_block_id(id: &Cow<str>) -> ValidatorResult {
below_len("Actions.block_id", 255, id)
}

impl<'a> Actions<'a> {
/// Build a new Actions block.
///
Expand Down
166 changes: 150 additions & 16 deletions src/blocks/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
//!
//! [context_docs]: https://api.slack.com/reference/block-kit/blocks#context
use std::borrow::Cow;

use serde::{Deserialize, Serialize};
use validator::Validate;

Expand All @@ -26,34 +28,42 @@ use crate::{convert, elems::Image, text, val_helpr::ValidationResult};
PartialEq,
Serialize,
Validate)]
pub struct Contents<'a> {
pub struct Context<'a> {
#[validate(length(max = 10))]
elements: Vec<ImageOrText<'a>>,

#[serde(skip_serializing_if = "Option::is_none")]
#[validate(length(max = 255))]
block_id: Option<String>,
#[validate(custom = "super::validate_block_id")]
block_id: Option<Cow<'a, str>>,
}

impl<'a> Contents<'a> {
impl<'a> Context<'a> {
/// Build a new Context block.
///
/// For example, see docs for ContextBuilder.
pub fn builder() -> build::ContextBuilderInit<'a> {
build::ContextBuilderInit::new()
}

/// Create an empty Context block (shorthand for `Default::default()`)
///
/// # Example
/// ```
/// use slack_blocks::{blocks::{context, Block},
/// text};
///
/// let context = context::Contents::new()
/// let context = context::Context::new()
/// .with_element(text::Plain::from("my unformatted text"));
///
/// let block: Block = context.into();
/// // < send block to slack's API >
/// ```
#[deprecated(since = "0.19.2", note = "use Context::builder")]
pub fn new() -> Self {
Default::default()
}

/// Set the `block_id` for interactions on an existing `context::Contents`
/// Set the `block_id` for interactions on an existing `context::Context`
///
/// # Arguments
/// - `block_id` - A string acting as a unique identifier for a block.
Expand All @@ -70,14 +80,15 @@ impl<'a> Contents<'a> {
/// text};
///
/// let text = text::Mrkdwn::from("_flavor_ *text*");
/// let context: Block = context::Contents::new().with_element(text)
/// .with_block_id("msg_id_12346")
/// .into();
/// let context: Block = context::Context::new().with_element(text)
/// .with_block_id("msg_id_12346")
/// .into();
///
/// // < send block to slack's API >
/// ```
pub fn with_block_id(mut self, block_id: impl ToString) -> Self {
self.block_id = Some(block_id.to_string());
#[deprecated(since = "0.19.2", note = "use Context::builder")]
pub fn with_block_id(mut self, block_id: impl Into<Cow<'a, str>>) -> Self {
self.block_id = Some(block_id.into());
self
}

Expand All @@ -97,20 +108,21 @@ impl<'a> Contents<'a> {
/// use slack_blocks::{blocks::{context, Block},
/// text};
///
/// let context = context::Contents::new()
/// let context = context::Context::new()
/// .with_element(text::Plain::from("my unformatted text"));
///
/// let block: Block = context.into();
/// // < send block to slack's API >
/// ```
#[deprecated(since = "0.19.2", note = "use Context::builder")]
pub fn with_element(mut self,
element: impl Into<self::ImageOrText<'a>>)
-> Self {
self.elements.push(element.into());
self
}

/// Construct a new `context::Contents` from a collection of
/// Construct a new `context::Context` from a collection of
/// composition objects that are may not be supported by Context
/// Blocks.
///
Expand All @@ -130,11 +142,12 @@ impl<'a> Contents<'a> {
/// pub fn main() {
/// let objs: Vec<text::Mrkdwn> = vec![text::Mrkdwn::from("*s i c k*"),
/// text::Mrkdwn::from("*t i g h t*"),];
/// let context = context::Contents::from_context_elements(objs);
/// let context = context::Context::from_context_elements(objs);
/// let block: Block = context.into();
/// // < send block to slack's API >
/// }
/// ```
#[deprecated(since = "0.19.2", note = "use Context::builder")]
pub fn from_context_elements(elements: impl IntoIterator<Item = impl Into<ImageOrText<'a>>>)
-> Self {
elements.into_iter()
Expand All @@ -157,7 +170,7 @@ impl<'a> Contents<'a> {
///
/// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
///
/// let block = blocks::context::Contents::new().with_block_id(long_string);
/// let block = blocks::context::Context::new().with_block_id(long_string);
///
/// assert_eq!(true, matches!(block.validate(), Err(_)));
/// ```
Expand All @@ -166,7 +179,128 @@ impl<'a> Contents<'a> {
}
}

impl<'a> From<Vec<ImageOrText<'a>>> for Contents<'a> {
/// Context block builder
pub mod build {
use std::marker::PhantomData;

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

/// Compile-time markers for builder methods
#[allow(non_camel_case_types)]
pub mod method {
/// ContextBuilder.elements
#[derive(Clone, Copy, Debug)]
pub struct elements;
}

/// Initial state for `ContextBuilder`
pub type ContextBuilderInit<'a> =
ContextBuilder<'a, RequiredMethodNotCalled<method::elements>>;

/// Build an Context block
///
/// Allows you to construct safely, with compile-time checks
/// on required setter methods.
///
/// # Required Methods
/// `ContextBuilder::build()` is only available if these methods have been called:
/// - `element`
///
/// # Example
/// ```
/// use slack_blocks::{blocks::Context, elems::Image, text::ToSlackPlaintext};
///
/// let block = Context::builder().element("foo".plaintext())
/// .element(Image::builder().image_url("foo.png")
/// .alt_text("pic of foo")
/// .build())
/// .build();
/// ```
#[derive(Debug)]
pub struct ContextBuilder<'a, Elements> {
elements: Option<Vec<ImageOrText<'a>>>,
block_id: Option<Cow<'a, str>>,
state: PhantomData<Elements>,
}

impl<'a, E> ContextBuilder<'a, E> {
/// Create a new ContextBuilder
pub fn new() -> Self {
Self { elements: None,
block_id: None,
state: PhantomData::<_> }
}

/// Add an `element` (**Required**)
///
/// A composition object; Must be image elements or text objects.
///
/// Maximum number of items is 10.
pub fn element<El>(self,
element: El)
-> ContextBuilder<'a, Set<method::elements>>
where El: Into<ImageOrText<'a>>
{
let mut elements = self.elements.unwrap_or_default();
elements.push(element.into());

ContextBuilder { block_id: self.block_id,
elements: Some(elements),
state: PhantomData::<_> }
}

/// Set `block_id` (Optional)
///
/// A string acting as a unique identifier for a block.
///
/// You can use this `block_id` when you receive an interaction payload
/// to [identify the source of the action 🔗].
///
/// If not specified, a `block_id` will be generated.
///
/// Maximum length for this field is 255 characters.
///
/// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
pub fn block_id<S>(mut self, block_id: S) -> Self
where S: Into<Cow<'a, str>>
{
self.block_id = Some(block_id.into());
self
}
}

impl<'a> ContextBuilder<'a, Set<method::elements>> {
/// All done building, now give me a darn actions block!
///
/// > `no method name 'build' found for struct 'ContextBuilder<...>'`?
/// Make sure all required setter methods have been called. See docs for `ContextBuilder`.
///
/// ```compile_fail
/// use slack_blocks::blocks::Context;
///
/// let foo = Context::builder().build(); // Won't compile!
/// ```
///
/// ```
/// use slack_blocks::{blocks::Context,
/// compose::text::ToSlackPlaintext,
/// elems::Image};
///
/// let block = Context::builder().element("foo".plaintext())
/// .element(Image::builder().image_url("foo.png")
/// .alt_text("pic of foo")
/// .build())
/// .build();
/// ```
pub fn build(self) -> Context<'a> {
Context { elements: self.elements.unwrap(),
block_id: self.block_id }
}
}
}

impl<'a> From<Vec<ImageOrText<'a>>> for Context<'a> {
fn from(elements: Vec<ImageOrText<'a>>) -> Self {
Self { elements,
..Default::default() }
Expand Down
11 changes: 8 additions & 3 deletions src/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
//!
//! [building block layouts 🔗]: https://api.slack.com/block-kit/building
use std::fmt;

use serde::{Deserialize, Serialize};

use crate::convert;
Expand All @@ -21,7 +23,7 @@ pub use actions::Actions;
#[doc(inline)]
pub mod context;
#[doc(inline)]
pub use context::Contents as Context;
pub use context::Context;

#[doc(inline)]
pub mod file;
Expand Down Expand Up @@ -89,8 +91,6 @@ pub enum Block<'a> {
File(File),
}

use std::fmt;

impl fmt::Display for Block<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match self {
Expand Down Expand Up @@ -140,3 +140,8 @@ convert!(impl<'a> From<Section<'a>> for Block<'a> => |a| Block::Section(a))
convert!(impl From<Image> for Block<'static> => |a| Block::Image(a));
convert!(impl<'a> From<Context<'a>> for Block<'a> => |a| Block::Context(a));
convert!(impl From<File> for Block<'static> => |a| Block::File(a));

fn validate_block_id(id: &std::borrow::Cow<str>)
-> crate::val_helpr::ValidatorResult {
crate::val_helpr::below_len("block_id", 255, id)
}

0 comments on commit da4d125

Please sign in to comment.