Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: crate should support header block and thoroughly document blox module #165

Merged
merged 3 commits into from
Jun 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions src/blocks/header.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! # Header Block
//!
//! [slack api docs 🔗]
//!
//! A header is a plain-text block that displays in a larger, bold font.
//!
//! Use it to delineate between different groups of content in your app's surfaces.
//!
//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#header

use std::borrow::Cow;

use serde::{Deserialize, Serialize};
#[cfg(feature = "validation")]
use validator::Validate;

use crate::text;
#[cfg(feature = "validation")]
use crate::val_helpr::*;

/// # Header Block
///
/// [slack api docs 🔗]
///
/// A header is a plain-text block that displays in a larger, bold font.
///
/// Use it to delineate between different groups of content in your app's surfaces.
///
/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#header
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
#[cfg_attr(feature = "validation", derive(Validate))]
pub struct Header<'a> {
#[cfg_attr(feature = "validation", validate(custom = "validate_text"))]
text: text::Text,

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

#[cfg(feature = "validation")]
fn validate_text(t: &text::Text) -> ValidatorResult {
below_len("text", 150, t)
}

impl<'a> Header<'a> {
/// Build a new Header block.
///
/// Alias for [`build::HeaderBuilder::new()`].
pub fn builder() -> build::HeaderBuilderInit<'a> {
build::HeaderBuilder::new()
}

/// Validate that this Header block agrees with Slack's model requirements
///
/// # Errors
/// - If `text` longer than 150 chars
/// - If `block_id` longer than 256 chars
///
/// # Example
/// ```
/// # use validator::Validate;
/// use slack_blocks::{blocks::Header, compose};
///
/// let long_string = |len| std::iter::repeat(' ').take(len).collect::<String>();
/// let assert_invalid = |block: &dyn Validate| match block.validate() {
/// | Ok(()) => panic!("validation should have failed"),
/// | Err(_) => (),
/// };
///
/// // block_id
/// let block = Header::builder().text("foo")
/// .block_id(long_string(256))
/// .build();
///
/// assert_invalid(&block);
///
/// // text
/// let block = Header::builder().text(long_string(151))
/// .block_id("foo")
/// .build();
///
/// assert_invalid(&block);
/// ```
#[cfg(feature = "validation")]
#[cfg_attr(docsrs, doc(cfg(feature = "validation")))]
pub fn validate(&self) -> ValidationResult {
todo!()
}
}

pub mod build {
//! [HeaderBuilder] and related items

use std::marker::PhantomData;

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

/// Methods of [`HeaderBuilder`]
#[allow(non_camel_case_types)]
pub mod method {
/// [`super::HeaderBuilder::text()`]
#[derive(Debug, Clone, Copy)]
pub struct text;
}

/// Initial state for [HeaderBuilder]
pub type HeaderBuilderInit<'a> =
HeaderBuilder<'a, RequiredMethodNotCalled<method::text>>;

/// Build an Header block
///
/// Allows you to construct safely, with compile-time checks
/// on required setter methods.
///
/// # Required Methods
/// `HeaderBuilder::build()` is only available if these methods have been called:
/// - `text`
///
/// # Example
/// ```
/// use slack_blocks::blocks::Header;
///
/// let block = Header::builder().block_id("foo").text("bar").build();
/// ```
#[derive(Clone, Debug)]
pub struct HeaderBuilder<'a, T> {
text: Option<text::Text>,
block_id: Option<Cow<'a, str>>,
state: PhantomData<T>,
}

impl<'a, T> HeaderBuilder<'a, T> {
/// Construct a new HeaderBuilder
pub fn new() -> Self {
Self { text: None,
block_id: None,
state: PhantomData::<_> }
}

/// Set `text` (**Required**)
///
/// The text for the block, in the form of a [`plain_text` text object 🔗].
///
/// Maximum length for the `text` in this field is 150 characters.
///
/// [`plain_text` text object 🔗]: https://api.slack.com/reference/messaging/composition-objects#text
pub fn text(self,
text: impl Into<text::Plain>)
-> HeaderBuilder<'a, Set<method::text>> {
HeaderBuilder { text: Some(text.into().into()),
block_id: self.block_id,
state: PhantomData::<_> }
}

/// XML child alias for [`Self::text()`]
#[cfg(feature = "blox")]
#[cfg_attr(docsrs, doc(cfg(feature = "blox")))]
pub fn child(self,
text: impl Into<text::Plain>)
-> HeaderBuilder<'a, Set<method::text>> {
self.text(text)
}

/// A string acting as a unique identifier for a block.
///
/// If not specified, one will be generated.
///
/// Maximum length for this field is 255 characters.
/// `block_id` should be unique for each message and each iteration of a message.
///
/// If a message is updated, use a new `block_id`.
pub fn block_id(mut self, block_id: impl Into<Cow<'a, str>>) -> Self {
self.block_id = Some(block_id.into());
self
}
}

impl<'a> HeaderBuilder<'a, Set<method::text>> {
/// All done building, now give me a darn actions block!
///
/// > `no method name 'build' found for struct 'FileBuilder<...>'`?
/// Make sure all required setter methods have been called. See docs for `FileBuilder`.
///
/// ```compile_fail
/// use slack_blocks::blocks::Header;
///
/// let foo = Header::builder().build(); // Won't compile!
/// ```
///
/// ```
/// use slack_blocks::blocks::Header;
///
/// let block = Header::builder().text("Foo").build();
/// ```
pub fn build(self) -> Header<'a> {
Header { text: self.text.unwrap(),
block_id: self.block_id }
}
}
}
18 changes: 11 additions & 7 deletions src/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,34 @@ use serde::{Deserialize, Serialize};

use crate::convert;

#[doc(inline)]
pub mod actions;
#[doc(inline)]
pub use actions::Actions;

#[doc(inline)]
pub mod context;
#[doc(inline)]
pub use context::Context;

#[doc(inline)]
pub mod file;
#[doc(inline)]
pub use file::File;

#[doc(inline)]
pub mod image;
#[doc(inline)]
pub use image::Image;

#[doc(inline)]
pub mod input;
#[doc(inline)]
pub use input::Input;

#[doc(inline)]
pub mod section;
#[doc(inline)]
pub use section::Section;

pub mod header;
#[doc(inline)]
pub use header::Header;

/// # Layout Blocks
///
/// Blocks are a series of components that can be combined
Expand All @@ -55,7 +53,7 @@ pub use section::Section;
/// You can include up to 50 blocks in each message, and 100 blocks in modals or home tabs.
///
/// [building block layouts 🔗]: https://api.slack.com/block-kit/building
#[derive(Serialize, Deserialize, Debug)]
#[derive(Hash, PartialEq, Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Block<'a> {
/// # Section Block
Expand Down Expand Up @@ -85,13 +83,17 @@ pub enum Block<'a> {
/// # Input Block
Input(Input<'a>),

/// # Input Block
Header(Header<'a>),

/// # File Block
File(File<'a>),
}

impl fmt::Display for Block<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match self {
| Block::Header { .. } => "Header",
| Block::Section { .. } => "Section",
| Block::Divider => "Divider",
| Block::Image { .. } => "Image",
Expand Down Expand Up @@ -128,6 +130,7 @@ impl<'a> Block<'a> {
| Actions(contents) => contents.validate(),
| Context(contents) => contents.validate(),
| Input(contents) => contents.validate(),
| Header(contents) => contents.validate(),
| File(contents) => contents.validate(),
| Divider => Ok(()),
}
Expand All @@ -140,6 +143,7 @@ convert!(impl<'a> From<Section<'a>> for Block<'a> => |a| Block::Section(a));
convert!(impl<'a> From<Image<'a>> for Block<'a> => |a| Block::Image(a));
convert!(impl<'a> From<Context<'a>> for Block<'a> => |a| Block::Context(a));
convert!(impl<'a> From<File<'a>> for Block<'a> => |a| Block::File(a));
convert!(impl<'a> From<Header<'a>> for Block<'a> => |a| Block::Header(a));

/// Error yielded when `TryFrom` is called on an unsupported block element.
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
Expand Down
Loading