Skip to content

Commit 81fd5d7

Browse files
authored
feat: file builder (#149)
1 parent 1a8594b commit 81fd5d7

File tree

3 files changed

+154
-37
lines changed

3 files changed

+154
-37
lines changed

src/blocks/file.rs

+150-16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
//! [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#file
88
//! [remote file 🔗]: https://api.slack.com/messaging/files/remote
99
10+
use std::borrow::Cow;
11+
1012
use serde::{Deserialize, Serialize};
1113
use validator::Validate;
1214

@@ -21,15 +23,22 @@ use crate::val_helpr::ValidationResult;
2123
/// [slack api docs 🔗]: https://api.slack.com/reference/block-kit/blocks#file
2224
/// [remote file 🔗]: https://api.slack.com/messaging/files/remote
2325
#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Serialize, Validate)]
24-
pub struct Contents {
25-
external_id: String,
26-
source: String,
26+
pub struct File<'a> {
27+
external_id: Cow<'a, str>,
28+
source: Cow<'a, str>,
2729
#[serde(skip_serializing_if = "Option::is_none")]
28-
#[validate(length(max = 255))]
29-
block_id: Option<String>,
30+
#[validate(custom = "super::validate_block_id")]
31+
block_id: Option<Cow<'a, str>>,
3032
}
3133

32-
impl Contents {
34+
impl<'a> File<'a> {
35+
/// Build a new File block.
36+
///
37+
/// For example, see docs for FileBuilder.
38+
pub fn builder() -> build::FileBuilderInit<'a> {
39+
build::FileBuilderInit::new()
40+
}
41+
3342
/// Create a file block from a [remote file 🔗]'s external ID.
3443
///
3544
/// # Arguments
@@ -51,14 +60,15 @@ impl Contents {
5160
/// # pub fn main() -> Result<(), Box<dyn Error>> {
5261
/// let file_id = upload_file_to_slack("https://www.cheese.com/cheese-wheel.png");
5362
///
54-
/// let block = blocks::file::Contents::from_external_id(file_id);
63+
/// let block = blocks::File::from_external_id(file_id);
5564
///
5665
/// // < send to slack API >
5766
/// # Ok(())
5867
/// # }
5968
/// ```
60-
pub fn from_external_id(external_file_id: impl AsRef<str>) -> Self {
61-
Self { external_id: external_file_id.as_ref().into(),
69+
#[deprecated(since = "0.19.3", note = "use File::builder")]
70+
pub fn from_external_id(external_file_id: impl Into<Cow<'a, str>>) -> Self {
71+
Self { external_id: external_file_id.into(),
6272
source: "remote".into(),
6373
block_id: None }
6474
}
@@ -86,15 +96,16 @@ impl Contents {
8696
/// # pub fn main() -> Result<(), Box<dyn Error>> {
8797
/// let file_id = upload_file_to_slack("https://www.cheese.com/cheese-wheel.png");
8898
///
89-
/// let block = blocks::file::Contents::from_external_id(file_id)
99+
/// let block = blocks::File::from_external_id(file_id)
90100
/// .with_block_id("my_file_in_a_block_1234");
91101
///
92102
/// // < send to slack API >
93103
/// # Ok(())
94104
/// # }
95105
/// ```
96-
pub fn with_block_id(mut self, block_id: impl AsRef<str>) -> Self {
97-
self.block_id = Some(block_id.as_ref().to_string());
106+
#[deprecated(since = "0.19.3", note = "use File::builder")]
107+
pub fn with_block_id(mut self, block_id: impl Into<Cow<'a, str>>) -> Self {
108+
self.block_id = Some(block_id.into());
98109
self
99110
}
100111

@@ -112,10 +123,8 @@ impl Contents {
112123
/// # pub fn main() -> Result<(), Box<dyn Error>> {
113124
/// let long_string = std::iter::repeat(' ').take(256).collect::<String>();
114125
///
115-
/// let block = blocks::file
116-
/// ::Contents
117-
/// ::from_external_id("file_id")
118-
/// .with_block_id(long_string);
126+
/// let block =
127+
/// blocks::File::from_external_id("file_id").with_block_id(long_string);
119128
///
120129
/// assert_eq!(true, matches!(block.validate(), Err(_)));
121130
///
@@ -127,3 +136,128 @@ impl Contents {
127136
Validate::validate(self)
128137
}
129138
}
139+
140+
/// File block builder
141+
pub mod build {
142+
use std::marker::PhantomData;
143+
144+
use super::*;
145+
use crate::build::*;
146+
147+
/// Compile-time markers for builder methods
148+
#[allow(non_camel_case_types)]
149+
pub mod method {
150+
/// FileBuilder.external_id
151+
#[derive(Clone, Copy, Debug)]
152+
pub struct external_id;
153+
}
154+
155+
/// Initial state for `FileBuilder`
156+
pub type FileBuilderInit<'a> =
157+
FileBuilder<'a, RequiredMethodNotCalled<method::external_id>>;
158+
159+
/// Build an File block
160+
///
161+
/// Allows you to construct safely, with compile-time checks
162+
/// on required setter methods.
163+
///
164+
/// # Required Methods
165+
/// `FileBuilder::build()` is only available if these methods have been called:
166+
/// - `external_id`
167+
/// - `source`
168+
///
169+
/// # Example
170+
/// ```
171+
/// use slack_blocks::{blocks::File, elems::Image, text::ToSlackPlaintext};
172+
///
173+
/// let my_file_id: String = {
174+
/// // use Slack Web API: files.remote.add to upload a file
175+
/// # "foo".into()
176+
/// };
177+
///
178+
/// let block = File::builder().external_id(my_file_id).build();
179+
/// ```
180+
#[derive(Debug)]
181+
pub struct FileBuilder<'a, ExternalId> {
182+
external_id: Option<Cow<'a, str>>,
183+
source: Option<Cow<'a, str>>,
184+
block_id: Option<Cow<'a, str>>,
185+
state: PhantomData<ExternalId>,
186+
}
187+
188+
impl<'a, Ext> FileBuilder<'a, Ext> {
189+
/// Create a new FileBuilder
190+
pub fn new() -> Self {
191+
Self { external_id: None,
192+
source: None,
193+
block_id: None,
194+
state: PhantomData::<_> }
195+
}
196+
197+
/// Set `external_id` (**Required**)
198+
///
199+
/// The external unique ID for a [remote file 🔗].
200+
///
201+
/// [remote file 🔗]: https://api.slack.com/messaging/files/remote
202+
pub fn external_id<S>(self,
203+
external_id: S)
204+
-> FileBuilder<'a, Set<method::external_id>>
205+
where S: Into<Cow<'a, str>>
206+
{
207+
FileBuilder { external_id: Some(external_id.into()),
208+
source: self.source,
209+
block_id: self.block_id,
210+
state: PhantomData::<_> }
211+
}
212+
213+
/// Set `block_id` (Optional)
214+
///
215+
/// A string acting as a unique identifier for a block.
216+
///
217+
/// You can use this `block_id` when you receive an interaction payload
218+
/// to [identify the source of the action 🔗].
219+
///
220+
/// If not specified, a `block_id` will be generated.
221+
///
222+
/// Maximum length for this field is 255 characters.
223+
///
224+
/// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
225+
pub fn block_id<S>(mut self, block_id: S) -> Self
226+
where S: Into<Cow<'a, str>>
227+
{
228+
self.block_id = Some(block_id.into());
229+
self
230+
}
231+
}
232+
233+
impl<'a> FileBuilder<'a, Set<method::external_id>> {
234+
/// All done building, now give me a darn actions block!
235+
///
236+
/// > `no method name 'build' found for struct 'FileBuilder<...>'`?
237+
/// Make sure all required setter methods have been called. See docs for `FileBuilder`.
238+
///
239+
/// ```compile_fail
240+
/// use slack_blocks::blocks::File;
241+
///
242+
/// let foo = File::builder().build(); // Won't compile!
243+
/// ```
244+
///
245+
/// ```
246+
/// use slack_blocks::{blocks::File,
247+
/// compose::text::ToSlackPlaintext,
248+
/// elems::Image};
249+
///
250+
/// let my_file_id: String = {
251+
/// // use Slack Web API: files.remote.add to upload a file
252+
/// # "foo".into()
253+
/// };
254+
///
255+
/// let block = File::builder().external_id(my_file_id).build();
256+
/// ```
257+
pub fn build(self) -> File<'a> {
258+
File { external_id: self.external_id.unwrap(),
259+
source: "remote".into(),
260+
block_id: self.block_id }
261+
}
262+
}
263+
}

src/blocks/mod.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ pub use context::Context;
2828
#[doc(inline)]
2929
pub mod file;
3030
#[doc(inline)]
31-
pub use file::Contents as File;
31+
pub use file::File;
3232

3333
#[doc(inline)]
3434
pub mod image;
@@ -88,7 +88,7 @@ pub enum Block<'a> {
8888
Input(Input<'a>),
8989

9090
/// # File Block
91-
File(File),
91+
File(File<'a>),
9292
}
9393

9494
impl fmt::Display for Block<'_> {
@@ -139,7 +139,7 @@ convert!(impl<'a> From<Input<'a>> for Block<'a> => |a| Block::Input(a));
139139
convert!(impl<'a> From<Section<'a>> for Block<'a> => |a| Block::Section(a));
140140
convert!(impl From<Image> for Block<'static> => |a| Block::Image(a));
141141
convert!(impl<'a> From<Context<'a>> for Block<'a> => |a| Block::Context(a));
142-
convert!(impl From<File> for Block<'static> => |a| Block::File(a));
142+
convert!(impl<'a> From<File<'a>> for Block<'a> => |a| Block::File(a));
143143

144144
fn validate_block_id(id: &std::borrow::Cow<str>)
145145
-> crate::val_helpr::ValidatorResult {

src/blocks/section.rs

+1-18
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ impl<'a> Contents<'a> {
139139
accessory: None }
140140
}
141141

142-
/// Set a unique `block_id` to identify this instance of an File Block.
142+
/// Set a unique `block_id` to identify this instance of an Section Block.
143143
///
144144
/// # Arguments
145145
///
@@ -152,23 +152,6 @@ impl<'a> Contents<'a> {
152152
/// If a message is updated, use a new `block_id`.
153153
///
154154
/// [identify the source of the action 🔗]: https://api.slack.com/interactivity/handling#payloads
155-
///
156-
/// # example
157-
/// ```
158-
/// use slack_blocks::blocks;
159-
///
160-
/// # fn upload_file_to_slack(s: &str) -> String { String::new() }
161-
/// # use std::error::Error;
162-
/// # pub fn main() -> Result<(), Box<dyn Error>> {
163-
/// let file_id = upload_file_to_slack("https://www.cheese.com/cheese-wheel.png");
164-
///
165-
/// let block = blocks::file::Contents::from_external_id(file_id)
166-
/// .with_block_id("my_file_in_a_block_1234");
167-
///
168-
/// // < send to slack API >
169-
/// # Ok(())
170-
/// # }
171-
/// ```
172155
pub fn with_block_id(mut self, block_id: impl Into<Cow<'a, str>>) -> Self {
173156
self.block_id = Some(block_id.into());
174157
self

0 commit comments

Comments
 (0)