Skip to content

Commit

Permalink
feat: context block (#19)
Browse files Browse the repository at this point in the history
* added context::Contents

* added with_block_id pattern
* added impl_from_contents!, `From<Contents> for Parent` pattern
  • Loading branch information
cakekindel authored Jun 15, 2020
1 parent f161d7b commit 202da69
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 51 deletions.
1 change: 0 additions & 1 deletion src/block_elements/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@ pub enum InputAttachment {
PlainInput,
RadioButtons,
}

1 change: 0 additions & 1 deletion src/block_elements/select.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,3 @@ pub struct ConversationSelect {}
/// public channels visible to the current user in the active workspace.
#[derive(Clone, Default, Debug, Deserialize, Hash, PartialEq, Serialize)]
pub struct ChannelSelect {}

72 changes: 61 additions & 11 deletions src/blocks/actions.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use validator::Validate;
use serde::{Serialize, Deserialize};

use crate::val_helpr::ValidationResult;
use crate::block_elements;
use crate::val_helpr::ValidationResult;

#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize, Validate)]
pub struct Contents {
Expand Down Expand Up @@ -33,15 +33,33 @@ pub struct Contents {
/// Maximum length for this field is 255 characters.
///
/// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
#[validate(length(max = 255))]
block_id: Option<String>,
}

impl Contents {
/// Create a empty Actions block
/// Create an empty Actions block
/// (uses `Default`)
pub fn new() -> Self {
Default::default()
}

/// Set the `block_id` for interactions on an existing `actions::Contents`
///
/// ```
/// use slack_blocks::blocks::{Block, actions};
///
/// pub fn main() {
/// let actions = actions::Contents::new().with_block_id("tally_ho");
/// let block: Block = actions.into();
/// // < send block to slack's API >
/// }
/// ```
pub fn with_block_id<StrIsh: AsRef<str>>(mut self, block_id: StrIsh) -> Self {
self.block_id = Some(block_id.as_ref().to_string());
self
}

/// Populate an Actions block with a collection of `BlockElement`s that
/// may not be supported by `Actions` blocks.
///
Expand All @@ -54,9 +72,26 @@ impl Contents {
/// For a list of `BlockElement` types that are, see `self::BlockElement`.
///
/// ### Runtime Validation
///
/// **only** validates that the block elements are compatible with `Actions`,
/// for full runtime model validation see the `validate` method.
pub fn from_elements<Els: Into<Vec<block_elements::BlockElement>>>(elements: Els) -> Result<Self, ()> {
///
/// ```
/// use slack_blocks::blocks::{Block, actions};
/// use slack_blocks::compose;
/// use slack_blocks::block_elements;
///
/// pub fn main() -> Result<(), ()> {
/// let btn = block_elements::BlockElement::Button;
/// let actions = actions::Contents::from_elements(vec![btn])?;
/// let block: Block = actions.into();
/// // < send block to slack's API >
/// Ok(())
/// }
/// ```
pub fn from_elements<Els: Into<Vec<block_elements::BlockElement>>>(
elements: Els,
) -> Result<Self, ()> {
elements // Into<Vec>
.into() // Vec
.try_into() // Result<Vec>
Expand All @@ -71,10 +106,25 @@ impl Contents {
/// ### Runtime Validation
/// **only** validates that the block elements are compatible with `Actions`,
/// for full runtime model validation see the `validate` method.
pub fn from_action_elements<Els: Into<Vec<self::BlockElement>>>(elements: Els) -> Self {
elements // Into<Vec>
.into() // Vec
.into() // Contents
///
/// ```
/// use slack_blocks::blocks::{Block, actions};
/// use slack_blocks::compose;
/// use slack_blocks::block_elements;
///
/// pub fn main() {
/// let btn = actions::BlockElement::Button;
/// let actions = actions::Contents::from_action_elements(vec![btn]);
/// let block: Block = actions.into();
/// // < send block to slack's API >
/// }
/// ```
pub fn from_action_elements<Els: IntoIterator<Item = impl Into<self::BlockElement>>>(elements: Els) -> Self {
elements
.into_iter()
.map(Into::<self::BlockElement>::into)
.collect::<Vec<_>>()
.into()
}

/// Validate the entire block and all of its
Expand All @@ -88,7 +138,8 @@ impl Contents {
impl TryFrom<Vec<block_elements::BlockElement>> for Contents {
type Error = ();
fn try_from(elements: Vec<block_elements::BlockElement>) -> Result<Self, Self::Error> {
elements.into_iter()
elements
.into_iter()
// Try to convert the bag of "any block element" to "block element supported by Actions"
.map(TryInto::try_into)
// If we hit one that is not supported, stop and continue with err
Expand Down Expand Up @@ -122,8 +173,8 @@ pub enum BlockElement {
impl TryFrom<block_elements::BlockElement> for self::BlockElement {
type Error = ();
fn try_from(el: block_elements::BlockElement) -> Result<Self, Self::Error> {
use block_elements::BlockElement as El;
use self::BlockElement::*;
use block_elements::BlockElement as El;

match el {
El::Button => Ok(Button),
Expand All @@ -137,4 +188,3 @@ impl TryFrom<block_elements::BlockElement> for self::BlockElement {
}
}
}

91 changes: 91 additions & 0 deletions src/blocks/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use serde::{Deserialize, Serialize};
use validator::Validate;

use crate::compose::Compose;
use crate::val_helpr::ValidationResult;

#[derive(Clone, Debug, Default, Deserialize, Hash, PartialEq, Serialize, Validate)]
pub struct Contents {
/// A collection of [image elements 🔗] and [text objects 🔗].
///
/// Maximum number of items is 10
/// [image elements 🔗]: https://api.slack.com/reference/messaging/block-elements#image
/// [text objects 🔗]: https://api.slack.com/reference/messaging/composition-objects#text
#[validate(length(max = 10))]
elements: Vec<Compose>,

/// 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
#[validate(length(max = 255))]
block_id: Option<String>,
}

impl Contents {
/// Construct a new empty Context block
/// (uses `Default`)
pub fn new() -> Self {
Default::default()
}

/// Set the `block_id` for interactions on an existing `context::Contents`
///
/// ```
/// use slack_blocks::blocks::{Block, context};
///
/// pub fn main() {
/// let context = context::Contents::new().with_block_id("tally_ho");
/// let block: Block = context.into();
/// // < send block to slack's API >
/// }
/// ```
pub fn with_block_id<StrIsh: AsRef<str>>(mut self, block_id: StrIsh) -> Self {
self.block_id = Some(block_id.as_ref().to_string());
self
}

/// Construct a new `context::Contents` from a bunch of
/// composition objects
///
/// ```
/// use slack_blocks::blocks::{Block, context};
/// use slack_blocks::compose;
///
/// pub fn main() {
/// let text = compose::Text::markdown("*s i c k*");
/// let context = context::Contents::from_elements(vec![text]);
/// let block: Block = context.into();
/// // < send block to slack's API >
/// }
/// ```
pub fn from_elements<Els: IntoIterator<Item = impl Into<Compose>>>(elements: Els) -> Self {
elements
.into_iter()
.map(Into::<Compose>::into)
.collect::<Vec<_>>()
.into()
}

/// Validate that the model agrees with slack's validation
/// requirements
pub fn validate(&self) -> ValidationResult {
Validate::validate(self)
}
}

// From impl backing the `from_elements` constructor
impl From<Vec<Compose>> for Contents {
fn from(elements: Vec<Compose>) -> Self {
Self {
elements,
..Default::default()
}
}
}
32 changes: 10 additions & 22 deletions src/blocks/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use serde::{Deserialize, Serialize};

pub mod image;
use crate::impl_from_contents;

pub mod actions;
pub mod context;
pub mod image;
pub mod section;

type ValidationResult = Result<(), validator::ValidationErrors>;
Expand Down Expand Up @@ -73,7 +76,7 @@ pub enum Block {
///
/// [context_docs]: https://api.slack.com/reference/block-kit/blocks#context
#[serde(rename = "context")]
Context {},
Context(context::Contents),

/// # Input Block
///
Expand Down Expand Up @@ -136,11 +139,16 @@ impl Block {
Section(contents) => contents.validate(),
Image(contents) => contents.validate(),
Actions(contents) => contents.validate(),
Context(contents) => contents.validate(),
other => todo!("validation not implemented for {}", other),
}
}
}

impl_from_contents!(Block, Section, section::Contents);
impl_from_contents!(Block, Actions, actions::Contents);
impl_from_contents!(Block, Context, context::Contents);

#[cfg(test)]
mod tests {
use test_case::test_case;
Expand Down Expand Up @@ -169,26 +177,6 @@ mod tests {
=> matches Block::Section (section::Contents::Fields(_));
"section_fields"
)]
#[test_case(
r#"{ "type": "context" }"#
=> matches Block::Context { .. };
"context"
)]
#[test_case(
r#"{ "type": "divider" }"#
=> matches Block::Divider;
"divider"
)]
#[test_case(
r#"{ "type": "input" }"#
=> matches Block::Input { .. };
"input"
)]
#[test_case(
r#"{ "type": "file" }"#
=> matches Block::File { .. };
"file"
)]
pub fn block_should_deserialize(json: &str) -> Block {
// arrange

Expand Down
33 changes: 28 additions & 5 deletions src/compose.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use serde::{Deserialize, Serialize};

use crate::impl_from_contents;

pub mod validation {
use crate::val_helpr::error;
use validator::ValidationError;
type ValidationResult = Result<(), ValidationError>;

pub fn text_is_plain(text: &super::Text) -> ValidationResult {
match text {
super::Text::Markdown { .. } => Err(error("text_is_plain", "expected plain, got markdown")),
super::Text::Markdown { .. } => {
Err(error("text_is_plain", "expected plain, got markdown"))
}
super::Text::Plain { .. } => Ok(()),
}
}
Expand All @@ -16,7 +20,7 @@ pub mod validation {
let len = text.text().chars().count();

if len > max_len {
let message = format!(
let message = format!(
"Section#text has max len of {}, but got text of len {}.",
max_len, len
);
Expand All @@ -28,6 +32,19 @@ pub mod validation {
}
}

/// # Composition Objects
///
/// Composition objects can be used inside of [block elements 🔗] and certain message payload fields.
///
/// They are simply common JSON object patterns that you'll encounter frequently
/// when building blocks or composing messages.
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
pub enum Compose {
Text(Text),
}

impl_from_contents!(Compose, Text, Text);

/// # Text Object
/// [_slack api docs 🔗_](https://api.slack.com/reference/block-kit/composition-objects#text)
///
Expand All @@ -36,7 +53,7 @@ pub mod validation {
/// or using [`mrkdwn` 🔗](https://api.slack.com/reference/surfaces/formatting),
/// our proprietary textual markup that's just different enough
/// from Markdown to frustrate you.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize)]
#[serde(tag = "type")]
pub enum Text {
/// ## Markdown text
Expand Down Expand Up @@ -116,13 +133,19 @@ pub enum Text {

impl Default for Text {
fn default() -> Self {
Text::Markdown { text: String::new(), verbatim: None, }
Text::Markdown {
text: String::new(),
verbatim: None,
}
}
}

impl Text {
pub fn plain<StrIsh: AsRef<str>>(text: StrIsh) -> Text {
Text::Plain { text: text.as_ref().to_string(), emoji: None }
Text::Plain {
text: text.as_ref().to_string(),
emoji: None,
}
}

pub fn markdown<StrIsh: AsRef<str>>(text: StrIsh) -> Text {
Expand Down
14 changes: 13 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
#[macro_use]
extern crate validator_derive;

pub mod blocks;
pub mod block_elements;
pub mod blocks;
pub mod compose;
pub mod val_helpr;

#[macro_export]
macro_rules! impl_from_contents {
($enum_name:ident, $variant:ident, $contents_type:ty) => {
impl From<$contents_type> for $enum_name {
fn from(contents: $contents_type) -> Self {
$enum_name::$variant(contents)
}
}
}
}

Loading

0 comments on commit 202da69

Please sign in to comment.