summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Woolcock <paul@woolcock.us>2018-08-23 21:23:59 -0400
committerPaul Woolcock <paul@woolcock.us>2018-08-23 21:46:31 -0400
commit49a2237803102cac939e068c74bba92aea4257fc (patch)
tree98d47b0e896110f2ce387c4cc82ec17938893686
parent6c2ebc61363585f129e61c2b9d97953fbadee28a (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.rs11
-rw-r--r--src/http_send.rs18
-rw-r--r--src/lib.rs109
-rw-r--r--src/macros.rs39
-rw-r--r--src/page.rs18
-rw-r--r--src/registration.rs54
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)?)
+ }
+}
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<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,
}