From 174a17109bb445c050f7bd666a2e9df45e69bc2d Mon Sep 17 00:00:00 2001 From: Paul Woolcock Date: Wed, 8 May 2019 22:07:26 -0400 Subject: [PATCH] Changes the StatusBuilder to be an...actual...builder This will enforce the invariant that statuses have to have either status text or a media_id --- src/lib.rs | 5 +- src/mastodon_client.rs | 4 +- src/status_builder.rs | 282 ++++++++++++++++++++++++++++++++++------- 3 files changed, 240 insertions(+), 51 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b30b37d..db3bd32 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,7 +127,7 @@ pub use requests::{ UpdateCredsRequest, UpdatePushRequest, }; -pub use status_builder::StatusBuilder; +pub use status_builder::{NewStatus, StatusBuilder}; /// Registering your App pub mod apps; @@ -160,6 +160,7 @@ pub mod prelude { pub use Data; pub use Mastodon; pub use MastodonClient; + pub use NewStatus; pub use Registration; pub use StatusBuilder; pub use StatusesRequest; @@ -318,7 +319,7 @@ impl MastodonClient for Mastodon { } /// Post a new status to the account. - fn new_status(&self, status: StatusBuilder) -> Result { + fn new_status(&self, status: NewStatus) -> Result { let response = self.send( self.client .post(&self.route("/api/v1/statuses")) diff --git a/src/mastodon_client.rs b/src/mastodon_client.rs index e1f8d01..d1defad 100644 --- a/src/mastodon_client.rs +++ b/src/mastodon_client.rs @@ -11,7 +11,7 @@ use requests::{ UpdateCredsRequest, UpdatePushRequest, }; -use status_builder::StatusBuilder; +use status_builder::NewStatus; /// Represents the set of methods that a Mastodon Client can do, so that /// implementations might be swapped out for testing @@ -189,7 +189,7 @@ pub trait MastodonClient { unimplemented!("This method was not implemented"); } /// POST /api/v1/statuses - fn new_status(&self, status: StatusBuilder) -> Result { + fn new_status(&self, status: NewStatus) -> Result { unimplemented!("This method was not implemented"); } /// GET /api/v1/timelines/public diff --git a/src/status_builder.rs b/src/status_builder.rs index 06a25ea..e1ce182 100644 --- a/src/status_builder.rs +++ b/src/status_builder.rs @@ -1,5 +1,4 @@ use isolang::Language; -use std::fmt; /// A builder pattern struct for constructing a status. /// @@ -9,36 +8,234 @@ use std::fmt; /// # extern crate elefren; /// # use elefren::{Language, StatusBuilder}; /// -/// let status = StatusBuilder { -/// status: "a status".to_string(), -/// sensitive: Some(true), -/// spoiler_text: Some("a CW".to_string()), -/// language: Some(Language::Eng), -/// ..Default::default() -/// }; +/// # fn main() -> Result<(), elefren::Error> { +/// let status = StatusBuilder::new() +/// .status("a status") +/// .sensitive(true) +/// .spoiler_text("a CW") +/// .language(Language::Eng) +/// .build()?; +/// # Ok(()) +/// # } /// ``` -#[derive(Debug, Default, Clone, Serialize, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct StatusBuilder { - /// The text of the status. - pub status: String, - /// Id of status being replied to. + status: Option, + in_reply_to_id: Option, + media_ids: Option>, + sensitive: Option, + spoiler_text: Option, + visibility: Option, + language: Option, +} + +impl StatusBuilder { + /// Create a StatusBuilder object + /// + /// # Example + /// + /// ```rust,no_run + /// # use elefren::prelude::*; + /// # use elefren::status_builder::Visibility; + /// # fn main() -> Result<(), elefren::Error> { + /// # let data = Data { + /// # base: "".into(), + /// # client_id: "".into(), + /// # client_secret: "".into(), + /// # redirect: "".into(), + /// # token: "".into(), + /// # }; + /// # let client = Mastodon::from(data); + /// let status = StatusBuilder::new() + /// .status("a status") + /// .visibility(Visibility::Public) + /// .build()?; + /// client.new_status(status)?; + /// # Ok(()) + /// # } + /// ``` + pub fn new() -> StatusBuilder { + StatusBuilder::default() + } + + /// Set the text for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new().status("awoooooo").build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn status>(&mut self, status: I) -> &mut Self { + self.status = Some(status.into()); + self + } + + /// Set the in_reply_to_id for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new() + /// .status("awooooo") + /// .in_reply_to("12345") + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn in_reply_to>(&mut self, id: I) -> &mut Self { + self.in_reply_to_id = Some(id.into()); + self + } + + /// Set the media_ids for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new().media_ids(&["foo", "bar"]).build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn media_ids>( + &mut self, + ids: I, + ) -> &mut Self { + self.media_ids = Some(ids.into_iter().map(|s| s.to_string()).collect::>()); + self + } + + /// Set the sensitive attribute for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new() + /// .media_ids(&["foo", "bar"]) + /// .sensitive(true) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn sensitive(&mut self, sensitive: bool) -> &mut Self { + self.sensitive = Some(sensitive); + self + } + + /// Set the spoiler text/CW for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new() + /// .status("awoooo!!") + /// .spoiler_text("awoo inside") + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn spoiler_text>(&mut self, spoiler_text: I) -> &mut Self { + self.spoiler_text = Some(spoiler_text.into()); + self + } + + /// Set the visibility for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # use elefren::status_builder::Visibility; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new() + /// .status("awooooooo") + /// .visibility(Visibility::Public) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn visibility(&mut self, visibility: Visibility) -> &mut Self { + self.visibility = Some(visibility); + self + } + + /// Set the language for the post + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # use elefren::Language; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new() + /// .status("awoo!!!!") + /// .language(Language::Eng) + /// .build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn language(&mut self, language: Language) -> &mut Self { + self.language = Some(language); + self + } + + /// Constructs a NewStatus + /// + /// # Example + /// + /// ```rust + /// # use elefren::prelude::*; + /// # fn main() -> Result<(), elefren::Error> { + /// let status = StatusBuilder::new().status("awoo!").build()?; + /// # Ok(()) + /// # } + /// ``` + pub fn build(&self) -> Result { + if self.status.is_none() && self.media_ids.is_none() { + return Err(crate::Error::Other( + "status text or media ids are required in order to post a status".to_string(), + )); + } + Ok(NewStatus { + status: self.status.clone(), + in_reply_to_id: self.in_reply_to_id.clone(), + media_ids: self.media_ids.clone(), + sensitive: self.sensitive.clone(), + spoiler_text: self.spoiler_text.clone(), + visibility: self.visibility.clone(), + language: self.language.clone(), + }) + } +} + +/// Represents a post that can be sent to the POST /api/v1/status endpoint +#[derive(Debug, Default, Clone, Serialize, PartialEq)] +pub struct NewStatus { + #[serde(skip_serializing_if = "Option::is_none")] + status: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub in_reply_to_id: Option, - /// Ids of media attachments being attached to the status. + in_reply_to_id: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub media_ids: Option>, - /// Whether current status is sensitive. + media_ids: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub sensitive: Option, - /// Text to precede the normal status text. + sensitive: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub spoiler_text: Option, - /// Visibility of the status, defaults to `Public`. + spoiler_text: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub visibility: Option, - /// Language code of the status + visibility: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub language: Option, + language: Option, } /// The visibility of a status. @@ -55,21 +252,6 @@ pub enum Visibility { Public, } -impl StatusBuilder { - /// Create a new status with text. - /// ``` - /// use elefren::prelude::*; - /// - /// let status = StatusBuilder::new("Hello World!"); - /// ``` - pub fn new(status: D) -> Self { - StatusBuilder { - status: status.to_string(), - ..Self::default() - } - } -} - impl Default for Visibility { fn default() -> Self { Visibility::Public @@ -84,9 +266,12 @@ mod tests { #[test] fn test_new() { - let s = StatusBuilder::new("a status"); - let expected = StatusBuilder { - status: "a status".to_string(), + let s = StatusBuilder::new() + .status("a status") + .build() + .expect("Couldn't build status"); + let expected = NewStatus { + status: Some("a status".to_string()), in_reply_to_id: None, media_ids: None, sensitive: None, @@ -125,17 +310,20 @@ mod tests { #[test] fn test_serialize_status() { - let status = StatusBuilder::new("a status"); + let status = StatusBuilder::new() + .status("a status") + .build() + .expect("Couldn't build status"); assert_eq!( serde_json::to_string(&status).expect("Couldn't serialize status"), "{\"status\":\"a status\"}".to_string() ); - let status = StatusBuilder { - status: "a status".into(), - language: Some(Language::Eng), - ..Default::default() - }; + let status = StatusBuilder::new() + .status("a status") + .language(Language::Eng) + .build() + .expect("Couldn't build status"); assert_eq!( serde_json::to_string(&status).expect("Couldn't serialize status"), "{\"status\":\"a status\",\"language\":\"eng\"}"