diff options
author | Paul Woolcock <paul@woolcock.us> | 2018-08-23 21:23:59 -0400 |
---|---|---|
committer | Paul Woolcock <paul@woolcock.us> | 2018-08-23 21:46:31 -0400 |
commit | 49a2237803102cac939e068c74bba92aea4257fc (patch) | |
tree | 98d47b0e896110f2ce387c4cc82ec17938893686 | |
parent | 6c2ebc61363585f129e61c2b9d97953fbadee28a (diff) |
Introduce HttpSend trait for converting `Request` -> `Response`
Parameterize everything that involves sending HTTP requests with the `H:
HttpSend` bound. This will allow us to swap out `HttpSend`
implementations when necessary, in order to better test our code
-rw-r--r-- | src/entities/itemsiter.rs | 11 | ||||
-rw-r--r-- | src/http_send.rs | 18 | ||||
-rw-r--r-- | src/lib.rs | 109 | ||||
-rw-r--r-- | src/macros.rs | 39 | ||||
-rw-r--r-- | src/page.rs | 18 | ||||
-rw-r--r-- | src/registration.rs | 54 |
6 files changed, 161 insertions, 88 deletions
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<T>, 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<Self::Item> { 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<Response>; + fn send(&self, client: &Client, builder: &mut RequestBuilder) -> Result<Response> { + let request = builder.build()?; + self.execute(client, request) + } +} + +pub struct HttpSender; + +impl HttpSend for HttpSender { + fn execute(&self, client: &Client, request: Request) -> Result<Response> { + Ok(client.execute(request)?) + } +} @@ -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<H: HttpSend = HttpSender> { 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<Page<Status>> { +pub trait MastodonClient<H: HttpSend = HttpSender> { + fn favourites(&self) -> Result<Page<Status, H>> { unimplemented!("This method was not implemented"); } - fn blocks(&self) -> Result<Page<Account>> { + fn blocks(&self) -> Result<Page<Account, H>> { unimplemented!("This method was not implemented"); } - fn domain_blocks(&self) -> Result<Page<String>> { + fn domain_blocks(&self) -> Result<Page<String, H>> { unimplemented!("This method was not implemented"); } - fn follow_requests(&self) -> Result<Page<Account>> { + fn follow_requests(&self) -> Result<Page<Account, H>> { unimplemented!("This method was not implemented"); } - fn get_home_timeline(&self) -> Result<Page<Status>> { + fn get_home_timeline(&self) -> Result<Page<Status, H>> { unimplemented!("This method was not implemented"); } - fn get_emojis(&self) -> Result<Page<Emoji>> { + fn get_emojis(&self) -> Result<Page<Emoji, H>> { unimplemented!("This method was not implemented"); } - fn mutes(&self) -> Result<Page<Account>> { + fn mutes(&self) -> Result<Page<Account, H>> { unimplemented!("This method was not implemented"); } - fn notifications(&self) -> Result<Page<Notification>> { + fn notifications(&self) -> Result<Page<Notification, H>> { unimplemented!("This method was not implemented"); } - fn reports(&self) -> Result<Page<Report>> { + fn reports(&self) -> Result<Page<Report, H>> { unimplemented!("This method was not implemented"); } - fn followers(&self, id: &str) -> Result<Page<Account>> { + fn followers(&self, id: &str) -> Result<Page<Account, H>> { unimplemented!("This method was not implemented"); } fn following(&self) -> Result<Account> { @@ -233,13 +238,13 @@ pub trait MastodonClient { fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> { unimplemented!("This method was not implemented"); } - fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status>> + fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status, H>> where S: Into<Option<StatusesRequest<'a>>>, { unimplemented!("This method was not implemented"); } - fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship>> { + fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship, H>> { unimplemented!("This method was not implemented"); } fn search_accounts( @@ -247,25 +252,30 @@ pub trait MastodonClient { query: &str, limit: Option<u64>, following: bool, - ) -> Result<Page<Account>> { + ) -> Result<Page<Account, H>> { unimplemented!("This method was not implemented"); } } -impl Mastodon { +impl<H: HttpSend> Mastodon<H> { 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<Response> { + Ok(self.http_sender.send( + &self.client, + req.headers(self.headers.clone()) + )?) } } -impl From<Data> for Mastodon { +impl From<Data> for Mastodon<HttpSender> { /// Creates a mastodon instance from the data struct. - fn from(data: Data) -> Mastodon { - let mut builder = MastodonBuilder::new(); + fn from(data: Data) -> Mastodon<HttpSender> { + let mut builder = MastodonBuilder::new(HttpSender); builder.data(data); builder .build() @@ -273,7 +283,7 @@ impl From<Data> for Mastodon { } } -impl MastodonClient for Mastodon { +impl<H: HttpSend> MastodonClient<H> for Mastodon<H> { 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<Account> { 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<Status> { - 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<Page<Status>> + fn statuses<'a, 'b: 'a, S>(&'b self, id: &'b str, request: S) -> Result<Page<Status, H>> where S: Into<Option<StatusesRequest<'a>>>, { @@ -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<Page<Relationship>> { + fn relationships(&self, ids: &[&str]) -> Result<Page<Relationship, H>> { 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<u64>, following: bool, - ) -> Result<Page<Account>> { + ) -> Result<Page<Account, H>> { 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<H: HttpSend> ops::Deref for Mastodon<H> { type Target = Data; fn deref(&self) -> &Self::Target { @@ -491,14 +505,16 @@ impl ops::Deref for Mastodon { } } -struct MastodonBuilder { +struct MastodonBuilder<H: HttpSend> { client: Option<Client>, + http_sender: H, data: Option<Data>, } -impl MastodonBuilder { - pub fn new() -> Self { +impl<H: HttpSend> MastodonBuilder<H> { + 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<Mastodon> { + pub fn build(self) -> Result<Mastodon<H>> { 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<T: for<'de> serde::Deserialize<'de>>(&self, url: String) -> Result<T> { - 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<Page<$ret>> { + fn $name(&self) -> Result<Page<$ret, H>> { 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<Page<$ret>> { + fn $name(&self, id: &str) -> Result<Page<$ret, H>> { 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<H>, next: Option<Url>, prev: Option<Url>, /// 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<Self> { + pub fn new(mastodon: &'a Mastodon<H>, response: Response) -> Result<Self> { 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<H: HttpSend> { base: String, client: Client, + http_sender: H, } #[derive(Deserialize)] @@ -32,7 +34,7 @@ struct AccessToken { access_token: String, } -impl Registration { +impl Registration<HttpSender> { /// 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<H: HttpSend> Registration<H> { + #[allow(dead_code)] + pub(crate) fn with_sender<I: Into<String>>(base: I, http_sender: H) -> Self { + Registration { + base: base.into(), + client: Client::new(), + http_sender: http_sender, + } + } + + fn send(&self, req: &mut RequestBuilder) -> Result<Response> { + 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<I: TryInto<App>>(self, app: I) -> Result<Registered> + pub fn register<I: TryInto<App>>(self, app: I) -> Result<Registered<H>> where Error: From<<I as TryInto<App>>::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<H: HttpSend> Registered<H> { + fn send(&self, req: &mut RequestBuilder) -> Result<Response> { + 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<String> { @@ -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<Mastodon> { + pub fn complete(self, code: String) -> Result<Mastodon<H>> { 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<H: HttpSend> { base: String, client: Client, client_id: String, client_secret: String, redirect: String, scopes: Scopes, + http_sender: H, } |