diff --git a/src/block_elements/select/multi/conversation.rs b/src/block_elements/select/multi/conversation.rs index b1a37dc..d2f730b 100644 --- a/src/block_elements/select/multi/conversation.rs +++ b/src/block_elements/select/multi/conversation.rs @@ -70,15 +70,16 @@ impl<'a> Conversation<'a> { /// ``` /// use slack_blocks::block_elements::select; /// - /// let select = select::Conversation::from_placeholder_and_action_id( - /// r#"Hey I really would appreciate it if you chose + /// let select = select::multi::Conversation::builder().placeholder( + /// r#"Hey I really would appreciate it if you chose /// a channel relatively soon, so that we can figure out /// where we need to send this poll, ok? it's kind of /// important that you specify where this poll should be /// sent, in case we haven't made that super clear. /// If you understand, could you pick a channel, already??"#, - /// "ABC123" - /// ); + /// ) + /// .action_id("ABC123") + /// .build(); /// /// assert!(matches!(select.validate(), Err(_))) /// ``` diff --git a/src/block_elements/select/multi/external.rs b/src/block_elements/select/multi/external.rs index a40375e..33654f4 100644 --- a/src/block_elements/select/multi/external.rs +++ b/src/block_elements/select/multi/external.rs @@ -76,15 +76,16 @@ impl<'a> External<'a> { /// ``` /// use slack_blocks::block_elements::select; /// - /// let select = select::External::from_placeholder_and_action_id( - /// r#"Hey I really would appreciate it if you chose + /// let placeholder = r#"Hey I really would appreciate it if you chose /// a channel relatively soon, so that we can figure out /// where we need to send this poll, ok? it's kind of /// important that you specify where this poll should be /// sent, in case we haven't made that super clear. - /// If you understand, could you pick a channel, already??"#, - /// "ABC123" - /// ); + /// If you understand, could you pick a channel, already??"#; + /// + /// let select = select::multi::External::builder().placeholder(placeholder) + /// .action_id("ABC123") + /// .build(); /// /// assert!(matches!(select.validate(), Err(_))) /// ``` diff --git a/src/block_elements/select/multi/mod.rs b/src/block_elements/select/multi/mod.rs index 4abf6b1..71563dd 100644 --- a/src/block_elements/select/multi/mod.rs +++ b/src/block_elements/select/multi/mod.rs @@ -11,10 +11,9 @@ //! [select menus 🔗]: https://api.slack.com/reference/block-kit/block-elements#select //! [guide to enabling interactivity 🔗]: https://api.slack.com/interactivity/handling -// mod builder; pub mod conversation; pub mod external; -// pub mod public_channel; +pub mod public_channel; pub mod static_; pub mod user; @@ -22,8 +21,8 @@ pub mod user; pub use conversation::Conversation; #[doc(inline)] pub use external::External; -// #[doc(inline)] -// pub use public_channel::PublicChannel; +#[doc(inline)] +pub use public_channel::PublicChannel; #[doc(inline)] pub use static_::Static; #[doc(inline)] diff --git a/src/block_elements/select/multi/public_channel.rs b/src/block_elements/select/multi/public_channel.rs new file mode 100644 index 0000000..c5a75ab --- /dev/null +++ b/src/block_elements/select/multi/public_channel.rs @@ -0,0 +1,75 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; +use validator::Validate; + +use crate::{compose::Confirm, + elems::select::public_channel::build, + text, + val_helpr::ValidationResult}; + +/// # Public Channel Select +/// [slack api docs 🔗](https://api.slack.com/reference/block-kit/block-elements#channel_select) +/// +/// This select menu will populate its options with a list of +/// public channels visible to the current user in the active workspace. +#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize, Validate)] +pub struct PublicChannel<'a> { + #[validate(custom = "crate::elems::select::validate::placeholder")] + pub(in crate::elems::select) placeholder: text::Text, + + #[validate(length(max = 255))] + pub(in crate::elems::select) action_id: Cow<'a, str>, + + #[serde(skip_serializing_if = "Option::is_none")] + #[validate] + pub(in crate::elems::select) confirm: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(in crate::elems::select) initial_channels: Option>, + + #[validate(range(min = 1))] + pub(in crate::elems::select) max_selected_items: Option, +} + +impl<'a> PublicChannel<'a> { + /// Build a new conversation multi-select element + /// + /// # Examples + /// ``` + /// // TODO(#130) + /// ``` + pub fn builder() -> build::MultiPublicChannelBuilderInit<'a> { + build::MultiPublicChannelBuilderInit::new() + } + + /// Validate that this Public Channel Select element + /// agrees with Slack's model requirements + /// + /// # Errors + /// - If `from_placeholder_and_action_id` was called with + /// `placeholder` longer than 150 chars + /// - If `from_placeholder_and_action_id` was called with + /// `action_id` longer than 255 chars + /// + /// # Example + /// ``` + /// use slack_blocks::block_elements::select; + /// + /// let select = select::multi::PublicChannel::builder().placeholder( + /// r#"Hey I really would appreciate it if you chose + /// a channel relatively soon, so that we can figure out + /// where we need to send this poll, ok? it's kind of + /// important that you specify where this poll should be + /// sent, in case we haven't made that super clear. + /// If you understand, could you pick a channel, already??"#, + /// ) + /// .action_id("ABC123") + /// .build(); + /// + /// assert!(matches!(select.validate(), Err(_))) + /// ``` + pub fn validate(&self) -> ValidationResult { + Validate::validate(&self) + } +} diff --git a/src/block_elements/select/multi/static_.rs b/src/block_elements/select/multi/static_.rs index 9b9ecce..69d0064 100644 --- a/src/block_elements/select/multi/static_.rs +++ b/src/block_elements/select/multi/static_.rs @@ -81,10 +81,10 @@ impl<'a> Static<'a> { /// sent, in case we haven't made that super clear. /// If you understand, could you pick a channel, already??"#; /// - /// let select = select::Static::builder().placeholder(placeholder) - /// .action_id("abc123") - /// .options(std::iter::empty()) - /// .build(); + /// let select = select::multi::Static::builder().placeholder(placeholder) + /// .action_id("abc123") + /// .options(std::iter::empty()) + /// .build(); /// /// assert!(matches!(select.validate(), Err(_))) /// ``` diff --git a/src/block_elements/select/multi/user.rs b/src/block_elements/select/multi/user.rs index 53a62f1..863a86e 100644 --- a/src/block_elements/select/multi/user.rs +++ b/src/block_elements/select/multi/user.rs @@ -63,15 +63,16 @@ impl<'a> User<'a> { /// ``` /// use slack_blocks::elems::select; /// - /// let select = select::User::from_placeholder_and_action_id( - /// r#"Hey I really would appreciate it if you chose + /// let select = select::multi::User::builder().placeholder( + /// r#"Hey I really would appreciate it if you chose /// a channel relatively soon, so that we can figure out /// where we need to send this poll, ok? it's kind of /// important that you specify where this poll should be /// sent, in case we haven't made that super clear. /// If you understand, could you pick a channel, already??"#, - /// "ABC123" - /// ); + /// ) + /// .action_id("ABC123") + /// .build(); /// /// assert!(matches!(select.validate(), Err(_))) /// ``` diff --git a/src/block_elements/select/public_channel.rs b/src/block_elements/select/public_channel.rs index 5c80c54..4bba332 100644 --- a/src/block_elements/select/public_channel.rs +++ b/src/block_elements/select/public_channel.rs @@ -27,6 +27,29 @@ pub struct PublicChannel<'a> { } impl<'a> PublicChannel<'a> { + /// Build a new public channel select element + /// + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// + /// use slack_blocks::{blocks::{Actions, Block}, + /// compose::Opt, + /// elems::{select, BlockElement}, + /// text}; + /// + /// let select: BlockElement = + /// select::PublicChannel::builder().placeholder("Choose your favorite channel!") + /// .action_id("fave_channel") + /// .build() + /// .into(); + /// + /// let block: Block = Actions::try_from(select).unwrap().into(); + /// ``` + pub fn builder() -> build::PublicChannelBuilderInit<'a> { + build::PublicChannelBuilderInit::new() + } + /// Construct a Select element, with a data /// source of the Public Channels in the user's /// Workspace. @@ -64,6 +87,8 @@ impl<'a> PublicChannel<'a> { /// /// // /// ``` + #[deprecated(since = "0.16.10", + note = "use elems::select::PublicChannel::builder instead.")] pub fn from_placeholder_and_action_id(placeholder: impl Into, action_id: impl Into>) -> Self { @@ -115,6 +140,8 @@ impl<'a> PublicChannel<'a> { /// /// // /// ``` + #[deprecated(since = "0.16.10", + note = "use elems::select::PublicChannel::builder instead.")] pub fn with_confirm(mut self, confirm: Confirm) -> Self { self.confirm = Some(confirm); self @@ -159,6 +186,8 @@ impl<'a> PublicChannel<'a> { /// /// // /// ``` + #[deprecated(since = "0.16.10", + note = "use elems::select::PublicChannel::builder instead.")] pub fn with_initial_channel(mut self, channel_id: impl Into>) -> Self { @@ -195,3 +224,244 @@ impl<'a> PublicChannel<'a> { Validate::validate(&self) } } + +pub mod build { + use std::marker::PhantomData; + + use super::*; + use crate::{build::*, + elems::select::{multi, select_kind}}; + + #[allow(non_camel_case_types)] + pub mod method { + pub struct placeholder; + pub struct action_id; + } + + /// PublicChannel Select builder + /// + /// Allows you to construct a PublicChannel Select safely, with compile-time checks + /// on required setter methods. + /// + /// # Required Methods + /// `PublicChannelBuilder::build()` is only available if these methods have been called: + /// - `placeholder` + /// - `action_id` + /// + /// NOTE: I'm experimenting with an API that deviates from the `from_foo_and_bar`. + /// If you're a user of this library, please give me feedback in the repository + /// as to which pattern you like more. This will most likely be the new builder pattern + /// for every structure in this crate. + /// + /// # Example + /// ``` + /// use std::convert::TryFrom; + /// + /// use slack_blocks::{blocks::{Actions, Block}, + /// compose::Opt, + /// elems::{select::PublicChannel, BlockElement}}; + /// + /// let select: BlockElement = + /// PublicChannel::builder().placeholder("Choose your favorite channel!") + /// .action_id("favorite_channel") + /// .build() + /// .into(); + /// + /// let block: Block = + /// Actions::try_from(select).expect("actions supports select elements") + /// .into(); + /// + /// // + /// ``` + #[derive(Default)] + pub struct PublicChannelBuilder<'a, Multi, Placeholder, ActionId> { + placeholder: Option, + action_id: Option>, + confirm: Option, + initial_channel: Option>, + initial_channels: Option>, + max_selected_items: Option, + state: PhantomData<(Multi, Placeholder, ActionId)>, + } + + pub type PublicChannelBuilderInit<'a> = + PublicChannelBuilder<'a, + select_kind::Single, + RequiredMethodNotCalled, + RequiredMethodNotCalled>; + + pub type MultiPublicChannelBuilderInit<'a> = + PublicChannelBuilder<'a, + select_kind::Multi, + RequiredMethodNotCalled, + RequiredMethodNotCalled>; + + // Methods that are always available + impl<'a, M, P, A> PublicChannelBuilder<'a, M, P, A> { + /// Construct a new PublicChannelBuilder + pub fn new() -> Self { + Self { placeholder: None, + action_id: None, + initial_channel: None, + initial_channels: None, + max_selected_items: None, + confirm: None, + state: PhantomData::<_> } + } + + /// Change the marker type params to some other arbitrary marker type params + fn cast_state(self) -> PublicChannelBuilder<'a, M, P2, A2> { + PublicChannelBuilder { placeholder: self.placeholder, + action_id: self.action_id, + confirm: self.confirm, + initial_channel: self.initial_channel, + initial_channels: self.initial_channels, + max_selected_items: self.max_selected_items, + state: PhantomData::<_> } + } + + /// Set `placeholder` (**Required**) + /// + /// A [`plain_text` only text object 🔗] that defines + /// the placeholder text shown on the menu. + /// Maximum length for the `text` in this field is 150 characters. + /// + /// [`plain_text` only text object 🔗]: https://api.slack.comhttps://api.slack.com/reference/block-kit/composition-objects#text + pub fn placeholder( + mut self, + text: impl Into) + -> PublicChannelBuilder<'a, M, Set, A> { + self.placeholder = Some(text.into().into()); + self.cast_state() + } + + /// Set `action_id` (**Required**) + /// + /// An identifier for the action triggered when a menu option is selected. + /// You can use this when you receive an interaction payload to [identify the source of the action 🔗]. + /// Should be unique among all other `action_id`s used elsewhere by your app. + /// Maximum length for this field is 255 characters. + /// + /// [identify the source of the action 🔗]: https://api.slack.comhttps://api.slack.com/interactivity/handling#payloads + pub fn action_id( + mut self, + text: impl Into>) + -> PublicChannelBuilder<'a, M, P, Set> { + self.action_id = Some(text.into()); + self.cast_state() + } + + /// Set `confirm` (Optional) + /// + /// A [confirm object 🔗] that defines an + /// optional confirmation dialog that appears after + /// a menu item is selected. + /// + /// [confirm object 🔗]: https://api.slack.comhttps://api.slack.com/reference/block-kit/composition-objects#confirm + pub fn confirm(mut self, confirm: Confirm) -> Self { + self.confirm = Some(confirm); + self + } + } + + impl<'a, M, P, A> PublicChannelBuilder<'a, M, P, A> { + /// Set `initial_channel` (Optional) + /// + /// The ID of any valid conversation to be pre-selected when the menu loads. + /// + /// If `default_to_current_conversation` is called, this will take precedence. + pub fn initial_channel(mut self, channel: S) -> Self + where S: Into> + { + self.initial_channel = Some(channel.into()); + self + } + + /// Set `initial_channel` (Optional, exclusive with `initial_channel_current`) + /// + /// A collection of IDs of any valid conversations to be pre-selected when the menu loads. + pub fn initial_channels(mut self, channels: S) -> Self + where S: Into> + { + self.initial_channels = Some(channels.into()); + self.cast_state() + } + } + + impl<'a, P, A> PublicChannelBuilder<'a, select_kind::Multi, P, A> { + /// Set `max_selected_items` (Optional) + /// + /// Specifies the maximum number of items that can be selected in the menu. + /// + /// Minimum number is 1. + pub fn max_selected_items(mut self, max: u32) -> Self { + self.max_selected_items = Some(max); + self + } + } + + impl<'a> + PublicChannelBuilder<'a, + select_kind::Single, + Set, + Set> + { + /// All done building, now give me a darn select element! + /// + /// > `no method name 'build' found for struct 'select::static_::build::PublicChannelBuilder<...>'`? + /// Make sure all required setter methods have been called. See docs for `PublicChannelBuilder`. + /// + /// ```compile_fail + /// use slack_blocks::elems::select::PublicChannel; + /// + /// let sel = PublicChannel::builder().build(); // Won't compile! + /// ``` + /// + /// ``` + /// use slack_blocks::elems::select::PublicChannel; + /// + /// let sel = PublicChannel::builder().placeholder("foo") + /// .action_id("bar") + /// .build(); + /// ``` + pub fn build(self) -> PublicChannel<'a> { + PublicChannel { placeholder: self.placeholder.unwrap(), + action_id: self.action_id.unwrap(), + confirm: self.confirm, + initial_channel: self.initial_channel } + } + } + + impl<'a> + PublicChannelBuilder<'a, + select_kind::Multi, + Set, + Set> + { + /// All done building, now give me a darn select element! + /// + /// > `no method name 'build' found for struct 'select::static_::build::PublicChannelBuilder<...>'`? + /// Make sure all required setter methods have been called. See docs for `PublicChannelBuilder`. + /// + /// ```compile_fail + /// use slack_blocks::elems::select; + /// + /// let sel = select::multi::PublicChannel::builder().build(); // Won't compile! + /// ``` + /// + /// ``` + /// use slack_blocks::elems::select; + /// + /// let sel = select::multi::PublicChannel::builder().placeholder("foo") + /// .action_id("bar") + /// .build(); + /// ``` + pub fn build(self) -> multi::PublicChannel<'a> { + multi::PublicChannel { placeholder: self.placeholder.unwrap(), + action_id: self.action_id.unwrap(), + confirm: self.confirm, + initial_channels: self.initial_channels, + max_selected_items: self.max_selected_items } + } + } +}