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
master
Paul Woolcock 6 years ago
parent 6c2ebc6136
commit 49a2237803
  1. 11
      src/entities/itemsiter.rs
  2. 18
      src/http_send.rs
  3. 109
      src/lib.rs
  4. 39
      src/macros.rs
  5. 18
      src/page.rs
  6. 54
      src/registration.rs

@ -1,4 +1,5 @@
use page::Page; use page::Page;
use http_send::HttpSend;
use serde::Deserialize; use serde::Deserialize;
/// Abstracts away the `next_page` logic into a single stream of items /// Abstracts away the `next_page` logic into a single stream of items
@ -23,15 +24,15 @@ use serde::Deserialize;
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>> { pub(crate) struct ItemsIter<'a, T: Clone + for<'de> Deserialize<'de>, H: 'a + HttpSend> {
page: Page<'a, T>, page: Page<'a, T, H>,
buffer: Vec<T>, buffer: Vec<T>,
cur_idx: usize, cur_idx: usize,
use_initial: bool, use_initial: bool,
} }
impl<'a, T: Clone + for<'de> Deserialize<'de>> 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>) -> ItemsIter<'a, T> { pub(crate) fn new(page: Page<'a, T, H>) -> ItemsIter<'a, T, H> {
ItemsIter { ItemsIter {
page, page,
buffer: vec![], 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; type Item = T;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {

@ -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}, header::{Authorization, Bearer, Headers},
Client, Client,
Response, Response,
RequestBuilder,
}; };
use entities::prelude::*; use entities::prelude::*;
use page::Page; use page::Page;
use http_send::{HttpSend, HttpSender};
pub use data::Data; pub use data::Data;
pub use errors::{ApiError, Error, Result}; pub use errors::{ApiError, Error, Result};
@ -69,6 +71,8 @@ pub mod data;
pub mod entities; pub mod entities;
/// Errors /// Errors
pub mod errors; pub mod errors;
/// Contains trait for converting `reqwest::Request`s to `reqwest::Response`s
pub mod http_send;
/// Handling multiple pages of entities. /// Handling multiple pages of entities.
pub mod page; pub mod page;
/// Registering your app. /// Registering your app.
@ -90,8 +94,9 @@ pub mod prelude {
/// Your mastodon application client, handles all requests to and from Mastodon. /// Your mastodon application client, handles all requests to and from Mastodon.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Mastodon { pub struct Mastodon<H: HttpSend = HttpSender> {
client: Client, client: Client,
http_sender: H,
headers: Headers, headers: Headers,
/// Raw data about your mastodon instance. /// Raw data about your mastodon instance.
pub data: Data, pub data: Data,
@ -100,35 +105,35 @@ pub struct Mastodon {
/// Represents the set of methods that a Mastodon Client can do, so that /// Represents the set of methods that a Mastodon Client can do, so that
/// implementations might be swapped out for testing /// implementations might be swapped out for testing
#[allow(unused)] #[allow(unused)]
pub trait MastodonClient { pub trait MastodonClient<H: HttpSend = HttpSender> {
fn favourites(&self) -> Result<Page<Status>> { fn favourites(&self) -> Result<Page<Status, H>> {
unimplemented!("This method was not implemented"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); 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"); unimplemented!("This method was not implemented");
} }
fn following(&self) -> Result<Account> { fn following(&self) -> Result<Account> {
@ -233,13 +238,13 @@ pub trait MastodonClient {
fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> { fn get_tagged_timeline(&self, hashtag: String, local: bool) -> Result<Vec<Status>> {
unimplemented!("This method was not implemented"); 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 where
S: Into<Option<StatusesRequest<'a>>>, S: Into<Option<StatusesRequest<'a>>>,
{ {
unimplemented!("This method was not implemented"); 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"); unimplemented!("This method was not implemented");
} }
fn search_accounts( fn search_accounts(
@ -247,25 +252,30 @@ pub trait MastodonClient {
query: &str, query: &str,
limit: Option<u64>, limit: Option<u64>,
following: bool, following: bool,
) -> Result<Page<Account>> { ) -> Result<Page<Account, H>> {
unimplemented!("This method was not implemented"); unimplemented!("This method was not implemented");
} }
} }
impl Mastodon { impl<H: HttpSend> Mastodon<H> {
methods![get, post, delete,]; methods![get, post, delete,];
fn route(&self, url: &str) -> String { fn route(&self, url: &str) -> String {
let mut s = (*self.base).to_owned(); format!("{}{}", self.base, url)
s += url; }
s
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. /// Creates a mastodon instance from the data struct.
fn from(data: Data) -> Mastodon { fn from(data: Data) -> Mastodon<HttpSender> {
let mut builder = MastodonBuilder::new(); let mut builder = MastodonBuilder::new(HttpSender);
builder.data(data); builder.data(data);
builder builder
.build() .build()
@ -273,7 +283,7 @@ impl From<Data> for Mastodon {
} }
} }
impl MastodonClient for Mastodon { impl<H: HttpSend> MastodonClient<H> for Mastodon<H> {
paged_routes! { paged_routes! {
(get) favourites: "favourites" => Status, (get) favourites: "favourites" => Status,
(get) blocks: "blocks" => Account, (get) blocks: "blocks" => Account,
@ -328,12 +338,11 @@ impl MastodonClient for Mastodon {
fn update_credentials(&self, changes: CredientialsBuilder) -> Result<Account> { fn update_credentials(&self, changes: CredientialsBuilder) -> Result<Account> {
let url = self.route("/api/v1/accounts/update_credentials"); let url = self.route("/api/v1/accounts/update_credentials");
let response = self let response = self.send(
.client self.client
.patch(&url) .patch(&url)
.headers(self.headers.clone()) .multipart(changes.into_form()?)
.multipart(changes.into_form()?) )?;
.send()?;
let status = response.status().clone(); let status = response.status().clone();
@ -348,12 +357,11 @@ impl MastodonClient for Mastodon {
/// Post a new status to the account. /// Post a new status to the account.
fn new_status(&self, status: StatusBuilder) -> Result<Status> { fn new_status(&self, status: StatusBuilder) -> Result<Status> {
let response = self let response = self.send(
.client self.client
.post(&self.route("/api/v1/statuses")) .post(&self.route("/api/v1/statuses"))
.headers(self.headers.clone()) .json(&status)
.json(&status) )?;
.send()?;
deserialise(response) deserialise(response)
} }
@ -423,7 +431,7 @@ impl MastodonClient for Mastodon {
/// # Ok(()) /// # 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 where
S: Into<Option<StatusesRequest<'a>>>, S: Into<Option<StatusesRequest<'a>>>,
{ {
@ -433,14 +441,16 @@ impl MastodonClient for Mastodon {
url = format!("{}{}", url, request.to_querystring()); 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) Page::new(self, response)
} }
/// Returns the client account's relationship to a list of other accounts. /// Returns the client account's relationship to a list of other accounts.
/// Such as whether they follow them or vice versa. /// 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?"); let mut url = self.route("/api/v1/accounts/relationships?");
if ids.len() == 1 { if ids.len() == 1 {
@ -455,7 +465,9 @@ impl MastodonClient for Mastodon {
url.pop(); 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) Page::new(self, response)
} }
@ -468,7 +480,7 @@ impl MastodonClient for Mastodon {
query: &str, query: &str,
limit: Option<u64>, limit: Option<u64>,
following: bool, following: bool,
) -> Result<Page<Account>> { ) -> Result<Page<Account, H>> {
let url = format!( let url = format!(
"{}/api/v1/accounts/search?q={}&limit={}&following={}", "{}/api/v1/accounts/search?q={}&limit={}&following={}",
self.base, self.base,
@ -477,13 +489,15 @@ impl MastodonClient for Mastodon {
following 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) Page::new(self, response)
} }
} }
impl ops::Deref for Mastodon { impl<H: HttpSend> ops::Deref for Mastodon<H> {
type Target = Data; type Target = Data;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
@ -491,14 +505,16 @@ impl ops::Deref for Mastodon {
} }
} }
struct MastodonBuilder { struct MastodonBuilder<H: HttpSend> {
client: Option<Client>, client: Option<Client>,
http_sender: H,
data: Option<Data>, data: Option<Data>,
} }
impl MastodonBuilder { impl<H: HttpSend> MastodonBuilder<H> {
pub fn new() -> Self { pub fn new(sender: H) -> Self {
MastodonBuilder { MastodonBuilder {
http_sender: sender,
client: None, client: None,
data: None, data: None,
} }
@ -515,7 +531,7 @@ impl MastodonBuilder {
self self
} }
pub fn build(self) -> Result<Mastodon> { pub fn build(self) -> Result<Mastodon<H>> {
Ok(if let Some(data) = self.data { Ok(if let Some(data) = self.data {
let mut headers = Headers::new(); let mut headers = Headers::new();
headers.set(Authorization(Bearer { headers.set(Authorization(Bearer {
@ -524,6 +540,7 @@ impl MastodonBuilder {
Mastodon { Mastodon {
client: self.client.unwrap_or_else(|| Client::new()), client: self.client.unwrap_or_else(|| Client::new()),
http_sender: self.http_sender,
headers, headers,
data, data,
} }

@ -4,9 +4,9 @@ macro_rules! methods {
fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String) fn $method<T: for<'de> serde::Deserialize<'de>>(&self, url: String)
-> Result<T> -> Result<T>
{ {
let response = self.client.$method(&url) let response = self.send(
.headers(self.headers.clone()) &mut self.client.$method(&url)
.send()?; )?;
deserialise(response) deserialise(response)
} }
@ -22,11 +22,11 @@ macro_rules! paged_routes {
"Equivalent to `/api/v1/", "Equivalent to `/api/v1/",
$url, $url,
"`\n# Errors\nIf `access_token` is not set."), "`\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 url = self.route(concat!("/api/v1/", $url));
let response = self.client.$method(&url) let response = self.send(
.headers(self.headers.clone()) &mut self.client.$method(&url)
.send()?; )?;
Page::new(self, response) Page::new(self, response)
} }
@ -55,10 +55,11 @@ macro_rules! route {
.file(stringify!($param), $param.as_ref())? .file(stringify!($param), $param.as_ref())?
)*; )*;
let response = self.client.post(&self.route(concat!("/api/v1/", $url))) let response = self.send(
.headers(self.headers.clone()) self.client
.multipart(form_data) .post(&self.route(concat!("/api/v1/", $url)))
.send()?; .multipart(form_data)
)?;
let status = response.status().clone(); let status = response.status().clone();
@ -90,10 +91,10 @@ macro_rules! route {
)* )*
}); });
let response = self.client.$method(&self.route(concat!("/api/v1/", $url))) let response = self.send(
.headers(self.headers.clone()) self.client.$method(&self.route(concat!("/api/v1/", $url)))
.json(&form_data) .json(&form_data)
.send()?; )?;
let status = response.status().clone(); let status = response.status().clone();
@ -152,11 +153,11 @@ macro_rules! paged_routes_with_id {
"Equivalent to `/api/v1/", "Equivalent to `/api/v1/",
$url, $url,
"`\n# Errors\nIf `access_token` is not set."), "`\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 url = self.route(&format!(concat!("/api/v1/", $url), id));
let response = self.client.$method(&url) let response = self.send(
.headers(self.headers.clone()) &mut self.client.$method(&url)
.send()?; )?;
Page::new(self, response) Page::new(self, response)
} }

@ -7,8 +7,10 @@ use reqwest::{
use serde::Deserialize; use serde::Deserialize;
use url::Url; use url::Url;
pub struct Page<'a, T: for<'de> Deserialize<'de>> { use http_send::HttpSend;
mastodon: &'a Mastodon,
pub struct Page<'a, T: for<'de> Deserialize<'de>, H: 'a + HttpSend> {
mastodon: &'a Mastodon<H>,
next: Option<Url>, next: Option<Url>,
prev: Option<Url>, prev: Option<Url>,
/// Initial set of items /// Initial set of items
@ -25,9 +27,9 @@ macro_rules! pages {
None => return Ok(None), None => return Ok(None),
}; };
let response = self.mastodon.client.get(url) let response = self.mastodon.send(
.headers(self.mastodon.headers.clone()) &mut self.mastodon.client.get(url)
.send()?; )?;
let (prev, next) = get_links(&response)?; let (prev, next) = get_links(&response)?;
self.next = next; 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! { pages! {
next: next_page, next: next_page,
prev: prev_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)?; let (prev, next) = get_links(&response)?;
Ok(Page { Ok(Page {
initial_items: deserialise(response)?, 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 /// Returns an iterator that provides a stream of `T`s
/// ///
/// This abstracts away the process of iterating over each item in a page, /// This abstracts away the process of iterating over each item in a page,

@ -1,4 +1,4 @@
use reqwest::Client; use reqwest::{Client, RequestBuilder, Response};
use try_from::TryInto; use try_from::TryInto;
use apps::{App, Scopes}; use apps::{App, Scopes};
@ -7,12 +7,14 @@ use Error;
use Mastodon; use Mastodon;
use MastodonBuilder; use MastodonBuilder;
use Result; use Result;
use http_send::{HttpSend, HttpSender};
/// Handles registering your mastodon app to your instance. It is recommended /// Handles registering your mastodon app to your instance. It is recommended
/// you cache your data struct to avoid registering on every run. /// you cache your data struct to avoid registering on every run.
pub struct Registration { pub struct Registration<H: HttpSend> {
base: String, base: String,
client: Client, client: Client,
http_sender: H,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -32,7 +34,7 @@ struct AccessToken {
access_token: String, access_token: String,
} }
impl Registration { impl Registration<HttpSender> {
/// Construct a new registration process to the instance of the `base` url. /// Construct a new registration process to the instance of the `base` url.
/// ``` /// ```
/// use elefren::apps::prelude::*; /// use elefren::apps::prelude::*;
@ -43,8 +45,27 @@ impl Registration {
Registration { Registration {
base: base.into(), base: base.into(),
client: Client::new(), 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. /// Register the application with the server from the `base` url.
/// ///
@ -69,13 +90,15 @@ impl Registration {
/// # Ok(()) /// # 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 where
Error: From<<I as TryInto<App>>::Err>, Error: From<<I as TryInto<App>>::Err>,
{ {
let app = app.try_into()?; let app = app.try_into()?;
let url = format!("{}/api/v1/apps", self.base); 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 { Ok(Registered {
base: self.base, base: self.base,
@ -84,11 +107,19 @@ impl Registration {
client_secret: oauth.client_secret, client_secret: oauth.client_secret,
redirect: oauth.redirect_uri, redirect: oauth.redirect_uri,
scopes: app.scopes(), 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 /// Returns the full url needed for authorisation. This needs to be opened
/// in a browser. /// in a browser.
pub fn authorize_url(&self) -> Result<String> { 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 /// Create an access token from the client id, client secret, and code
/// provided by the authorisation url. /// 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!( let url = format!(
"{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}", "{}/oauth/token?client_id={}&client_secret={}&code={}&grant_type=authorization_code&redirect_uri={}",
self.base, self.base,
@ -112,7 +143,9 @@ impl Registered {
self.redirect 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 { let data = Data {
base: self.base.into(), base: self.base.into(),
@ -122,17 +155,18 @@ impl Registered {
token: token.access_token.into(), token: token.access_token.into(),
}; };
let mut builder = MastodonBuilder::new(); let mut builder = MastodonBuilder::new(self.http_sender);
builder.client(self.client).data(data); builder.client(self.client).data(data);
Ok(builder.build()?) Ok(builder.build()?)
} }
} }
pub struct Registered { pub struct Registered<H: HttpSend> {
base: String, base: String,
client: Client, client: Client,
client_id: String, client_id: String,
client_secret: String, client_secret: String,
redirect: String, redirect: String,
scopes: Scopes, scopes: Scopes,
http_sender: H,
} }

Loading…
Cancel
Save