diff --git a/src/entities/itemsiter.rs b/src/entities/itemsiter.rs index 6b7db1b..119404b 100644 --- a/src/entities/itemsiter.rs +++ b/src/entities/itemsiter.rs @@ -1,4 +1,5 @@ use page::Page; +use http_send::HttpSend; use serde::Deserialize; /// Abstracts away the `next_page` logic into a single stream of items @@ -23,15 +24,15 @@ use serde::Deserialize; /// # Ok(()) /// # } /// ``` -pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>> { - page: Page<'a, T>, +pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>, H: 'a + HttpSend> { + page: Page<'a, T, H>, buffer: Vec, cur_idx: usize, use_initial: bool, } -impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<'a, T> { - pub(crate) fn new(page: Page<'a, T>) -> ItemsIter<'a, T> { +impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> ItemsIter<'a, T, H> { + pub(crate) fn new(page: Page<'a, T, H>) -> ItemsIter<'a, T, H> { ItemsIter { page, buffer: vec![], @@ -60,7 +61,7 @@ impl<'a, T: Clone + for<'de> Deserialize<'de>> ItemsIter<'a, T> { } } -impl<'a, T: Clone + for<'de> Deserialize<'de>> Iterator for ItemsIter<'a, T> { +impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> Iterator for ItemsIter<'a, T, H> { type Item = T; fn next(&mut self) -> Option { diff --git a/src/http_send.rs b/src/http_send.rs new file mode 100644 index 0000000..6257601 --- /dev/null +++ b/src/http_send.rs @@ -0,0 +1,18 @@ +use Result; +use reqwest::{Client, Request, RequestBuilder, Response}; + +pub trait HttpSend { + fn execute(&self, client: &Client, request: Request) -> Result; + fn send(&self, client: &Client, builder: &mut RequestBuilder) -> Result { + let request = builder.build()?; + self.execute(client, request) + } +} + +pub struct HttpSender; + +impl HttpSend for HttpSender { + fn execute(&self, client: &Client, request: Request) -> Result { + Ok(client.execute(request)?) + } +} diff --git a/src/lib.rs b/src/lib.rs index f7f1306..532ae7a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,10 +50,12 @@ use reqwest::{ header::{Authorization, Bearer, Headers}, Client, Response, + RequestBuilder, }; use entities::prelude::*; use page::Page; +use http_send::{HttpSend, HttpSender}; pub use data::Data; pub use errors::{ApiError, Error, Result}; @@ -69,6 +71,8 @@ pub mod data; pub mod entities; /// Errors pub mod errors; +/// Contains trait for converting `reqwest::Request`s to `reqwest::Response`s +pub mod http_send; /// Handling multiple pages of entities. pub mod page; /// Registering your app. @@ -90,8 +94,9 @@ pub mod prelude { /// Your mastodon application client, handles all requests to and from Mastodon. #[derive(Clone, Debug)] -pub struct Mastodon { +pub struct Mastodon { client: Client, + http_sender: H, headers: Headers, /// Raw data about your mastodon instance. pub data: Data, @@ -100,35 +105,35 @@ pub struct Mastodon { /// Represents the set of methods that a Mastodon Client can do, so that /// implementations might be swapped out for testing #[allow(unused)] -pub trait MastodonClient { - fn favourites(&self) -> Result> { +pub trait MastodonClient { + fn favourites(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn blocks(&self) -> Result> { + fn blocks(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn domain_blocks(&self) -> Result> { + fn domain_blocks(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn follow_requests(&self) -> Result> { + fn follow_requests(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn get_home_timeline(&self) -> Result> { + fn get_home_timeline(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn get_emojis(&self) -> Result> { + fn get_emojis(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn mutes(&self) -> Result> { + fn mutes(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn notifications(&self) -> Result> { + fn notifications(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn reports(&self) -> Result> { + fn reports(&self) -> Result> { unimplemented!("This method was not implemented"); } - fn followers(&self, id: &str) -> Result> { + fn followers(&self, id: &str) -> Result> { unimplemented!("This method was not implemented"); } fn following(&self) -> Result { @@ -233,13 +238,13 @@ pub trait MastodonClient { fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result> { unimplemented!("This method was not implemented"); } - fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> + fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> where S: Into>>, { unimplemented!("This method was not implemented"); } - fn relationships(&self, ids: &[&str]) -> Result> { + fn relationships(&self, ids: &[&str]) -> Result> { unimplemented!("This method was not implemented"); } fn search_accounts( @@ -247,25 +252,30 @@ pub trait MastodonClient { query: &str, limit: Option, following: bool, - ) -> Result> { + ) -> Result> { unimplemented!("This method was not implemented"); } } -impl Mastodon { +impl Mastodon { methods![get, post, delete,]; fn route(&self, url: &str) -> String { - let mut s = (*self.base).to_owned(); - s += url; - s + format!("{}{}", self.base, url) + } + + pub(crate) fn send(&self, req: &mut RequestBuilder) -> Result { + Ok(self.http_sender.send( + &self.client, + req.headers(self.headers.clone()) + )?) } } -impl From for Mastodon { +impl From for Mastodon { /// Creates a mastodon instance from the data struct. - fn from(data: Data) -> Mastodon { - let mut builder = MastodonBuilder::new(); + fn from(data: Data) -> Mastodon { + let mut builder = MastodonBuilder::new(HttpSender); builder.data(data); builder .build() @@ -273,7 +283,7 @@ impl From for Mastodon { } } -impl MastodonClient for Mastodon { +impl MastodonClient for Mastodon { paged_routes! { (get) favourites: "favourites" => Status, (get) blocks: "blocks" => Account, @@ -328,12 +338,11 @@ impl MastodonClient for Mastodon { fn update_credentials(&self, changes: CredientialsBuilder) -> Result { let url = self.route("/api/v1/accounts/update_credentials"); - let response = self - .client - .patch(&url) - .headers(self.headers.clone()) - .multipart(changes.into_form()?) - .send()?; + let response = self.send( + self.client + .patch(&url) + .multipart(changes.into_form()?) + )?; let status = response.status().clone(); @@ -348,12 +357,11 @@ impl MastodonClient for Mastodon { /// Post a new status to the account. fn new_status(&self, status: StatusBuilder) -> Result { - let response = self - .client - .post(&self.route("/api/v1/statuses")) - .headers(self.headers.clone()) - .json(&status) - .send()?; + let response = self.send( + self.client + .post(&self.route("/api/v1/statuses")) + .json(&status) + )?; deserialise(response) } @@ -423,7 +431,7 @@ impl MastodonClient for Mastodon { /// # Ok(()) /// # } /// ``` - fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> + fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result> where S: Into>>, { @@ -433,14 +441,16 @@ impl MastodonClient for Mastodon { url = format!("{}{}", url, request.to_querystring()); } - let response = self.client.get(&url).headers(self.headers.clone()).send()?; + let response = self.send( + &mut self.client.get(&url) + )?; Page::new(self, response) } /// Returns the client account's relationship to a list of other accounts. /// Such as whether they follow them or vice versa. - fn relationships(&self, ids: &[&str]) -> Result> { + fn relationships(&self, ids: &[&str]) -> Result> { let mut url = self.route("/api/v1/accounts/relationships?"); if ids.len() == 1 { @@ -455,7 +465,9 @@ impl MastodonClient for Mastodon { url.pop(); } - let response = self.client.get(&url).headers(self.headers.clone()).send()?; + let response = self.send( + &mut self.client.get(&url) + )?; Page::new(self, response) } @@ -468,7 +480,7 @@ impl MastodonClient for Mastodon { query: &str, limit: Option, following: bool, - ) -> Result> { + ) -> Result> { let url = format!( "{}/api/v1/accounts/search?q={}&limit={}&following={}", self.base, @@ -477,13 +489,15 @@ impl MastodonClient for Mastodon { following ); - let response = self.client.get(&url).headers(self.headers.clone()).send()?; + let response = self.send( + &mut self.client.get(&url) + )?; Page::new(self, response) } } -impl ops::Deref for Mastodon { +impl ops::Deref for Mastodon { type Target = Data; fn deref(&self) -> &Self::Target { @@ -491,14 +505,16 @@ impl ops::Deref for Mastodon { } } -struct MastodonBuilder { +struct MastodonBuilder { client: Option, + http_sender: H, data: Option, } -impl MastodonBuilder { - pub fn new() -> Self { +impl MastodonBuilder { + pub fn new(sender: H) -> Self { MastodonBuilder { + http_sender: sender, client: None, data: None, } @@ -515,7 +531,7 @@ impl MastodonBuilder { self } - pub fn build(self) -> Result { + pub fn build(self) -> Result> { Ok(if let Some(data) = self.data { let mut headers = Headers::new(); headers.set(Authorization(Bearer { @@ -524,6 +540,7 @@ impl MastodonBuilder { Mastodon { client: self.client.unwrap_or_else(|| Client::new()), + http_sender: self.http_sender, headers, data, } diff --git a/src/macros.rs b/src/macros.rs index f3347cc..d47a860 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -4,9 +4,9 @@ macro_rules! methods { fn $method serde::Deserialize<'de>>(&self, url: String) -> Result { - let response = self.client.$method(&url) - .headers(self.headers.clone()) - .send()?; + let response = self.send( + &mut self.client.$method(&url) + )?; deserialise(response) } @@ -22,11 +22,11 @@ macro_rules! paged_routes { "Equivalent to `/api/v1/", $url, "`\n# Errors\nIf `access_token` is not set."), - fn $name(&self) -> Result> { + fn $name(&self) -> Result> { let url = self.route(concat!("/api/v1/", $url)); - let response = self.client.$method(&url) - .headers(self.headers.clone()) - .send()?; + let response = self.send( + &mut self.client.$method(&url) + )?; Page::new(self, response) } @@ -55,10 +55,11 @@ macro_rules! route { .file(stringify!($param), $param.as_ref())? )*; - let response = self.client.post(&self.route(concat!("/api/v1/", $url))) - .headers(self.headers.clone()) - .multipart(form_data) - .send()?; + let response = self.send( + self.client + .post(&self.route(concat!("/api/v1/", $url))) + .multipart(form_data) + )?; let status = response.status().clone(); @@ -90,10 +91,10 @@ macro_rules! route { )* }); - let response = self.client.$method(&self.route(concat!("/api/v1/", $url))) - .headers(self.headers.clone()) - .json(&form_data) - .send()?; + let response = self.send( + self.client.$method(&self.route(concat!("/api/v1/", $url))) + .json(&form_data) + )?; let status = response.status().clone(); @@ -152,11 +153,11 @@ macro_rules! paged_routes_with_id { "Equivalent to `/api/v1/", $url, "`\n# Errors\nIf `access_token` is not set."), - fn $name(&self, id: &str) -> Result> { + fn $name(&self, id: &str) -> Result> { let url = self.route(&format!(concat!("/api/v1/", $url), id)); - let response = self.client.$method(&url) - .headers(self.headers.clone()) - .send()?; + let response = self.send( + &mut self.client.$method(&url) + )?; Page::new(self, response) } diff --git a/src/page.rs b/src/page.rs index 93d39e3..5df4f99 100644 --- a/src/page.rs +++ b/src/page.rs @@ -7,8 +7,10 @@ use reqwest::{ use serde::Deserialize; use url::Url; -pub struct Page<'a, T: for<'de> Deserialize<'de>> { - mastodon: &'a Mastodon, +use http_send::HttpSend; + +pub struct Page<'a, T: for<'de> Deserialize<'de>, H: 'a + HttpSend> { + mastodon: &'a Mastodon, next: Option, prev: Option, /// Initial set of items @@ -25,9 +27,9 @@ macro_rules! pages { None => return Ok(None), }; - let response = self.mastodon.client.get(url) - .headers(self.mastodon.headers.clone()) - .send()?; + let response = self.mastodon.send( + &mut self.mastodon.client.get(url) + )?; let (prev, next) = get_links(&response)?; self.next = next; @@ -39,13 +41,13 @@ macro_rules! pages { } } -impl<'a, T: for<'de> Deserialize<'de>> Page<'a, T> { +impl<'a, T: for<'de> Deserialize<'de>, H: HttpSend> Page<'a, T, H> { pages! { next: next_page, prev: prev_page } - pub fn new(mastodon: &'a Mastodon, response: Response) -> Result { + pub fn new(mastodon: &'a Mastodon, response: Response) -> Result { let (prev, next) = get_links(&response)?; Ok(Page { initial_items: deserialise(response)?, @@ -56,7 +58,7 @@ impl<'a, T: for<'de> Deserialize<'de>> Page<'a, T> { } } -impl<'a, T: Clone + for<'de> Deserialize<'de>> Page<'a, T> { +impl<'a, T: Clone + for<'de> Deserialize<'de>, H: HttpSend> Page<'a, T, H> { /// Returns an iterator that provides a stream of `T`s /// /// This abstracts away the process of iterating over each item in a page, diff --git a/src/registration.rs b/src/registration.rs index 060bc68..7c20f3f 100644 --- a/src/registration.rs +++ b/src/registration.rs @@ -1,4 +1,4 @@ -use reqwest::Client; +use reqwest::{Client, RequestBuilder, Response}; use try_from::TryInto; use apps::{App, Scopes}; @@ -7,12 +7,14 @@ use Error; use Mastodon; use MastodonBuilder; use Result; +use http_send::{HttpSend, HttpSender}; /// Handles registering your mastodon app to your instance. It is recommended /// you cache your data struct to avoid registering on every run. -pub struct Registration { +pub struct Registration { base: String, client: Client, + http_sender: H, } #[derive(Deserialize)] @@ -32,7 +34,7 @@ struct AccessToken { access_token: String, } -impl Registration { +impl Registration { /// Construct a new registration process to the instance of the `base` url. /// ``` /// use elefren::apps::prelude::*; @@ -43,8 +45,27 @@ impl Registration { Registration { base: base.into(), client: Client::new(), + http_sender: HttpSender, } } +} + +impl Registration { + #[allow(dead_code)] + pub(crate) fn with_sender>(base: I, http_sender: H) -> Self { + Registration { + base: base.into(), + client: Client::new(), + http_sender: http_sender, + } + } + + fn send(&self, req: &mut RequestBuilder) -> Result { + Ok(self.http_sender.send( + &self.client, + req + )?) + } /// Register the application with the server from the `base` url. /// @@ -69,13 +90,15 @@ impl Registration { /// # Ok(()) /// # } /// ``` - pub fn register>(self, app: I) -> Result + pub fn register>(self, app: I) -> Result> where Error: From<>::Err>, { let app = app.try_into()?; let url = format!("{}/api/v1/apps", self.base); - let oauth: OAuth = self.client.post(&url).form(&app).send()?.json()?; + let oauth: OAuth = self.send( + self.client.post(&url).form(&app) + )?.json()?; Ok(Registered { base: self.base, @@ -84,11 +107,19 @@ impl Registration { client_secret: oauth.client_secret, redirect: oauth.redirect_uri, scopes: app.scopes(), + http_sender: self.http_sender, }) } } -impl Registered { +impl Registered { + fn send(&self, req: &mut RequestBuilder) -> Result { + Ok(self.http_sender.send( + &self.client, + req + )?) + } + /// Returns the full url needed for authorisation. This needs to be opened /// in a browser. pub fn authorize_url(&self) -> Result { @@ -102,7 +133,7 @@ impl Registered { /// Create an access token from the client id, client secret, and code /// provided by the authorisation url. - pub fn complete(self, code: String) -> Result { + pub fn complete(self, code: String) -> Result> { let url = format!( "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}", self.base, @@ -112,7 +143,9 @@ impl Registered { self.redirect ); - let token: AccessToken = self.client.post(&url).send()?.json()?; + let token: AccessToken = self.send( + &mut self.client.post(&url) + )?.json()?; let data = Data { base: self.base.into(), @@ -122,17 +155,18 @@ impl Registered { token: token.access_token.into(), }; - let mut builder = MastodonBuilder::new(); + let mut builder = MastodonBuilder::new(self.http_sender); builder.client(self.client).data(data); Ok(builder.build()?) } } -pub struct Registered { +pub struct Registered { base: String, client: Client, client_id: String, client_secret: String, redirect: String, scopes: Scopes, + http_sender: H, }