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 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> {

@ -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,
}

@ -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)
}

@ -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,

@ -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,
}

Loading…
Cancel
Save