summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Woolcock <paul@woolcock.us>2020-10-07 05:47:39 -0400
committerPaul Woolcock <paul@woolcock.us>2020-10-07 09:06:13 -0400
commit02ca0a89515413ac9fb3b655de2f21f6a711e0f2 (patch)
tree004bcd9f88eca168e10e1ac85c5987fdd6769fcf
parent04b5b54212629f058bdab1ba55c89a3d417e0454 (diff)
Add basic async client
This adds a module, accessible by compiling with `--features async`, that provides an `elefren::async::Client`. The client is runtime-agnostic, and currently only provides unauthenticated access, see the docs for the full list of methods that can be performed* with this client. * note that some API calls are publicly available by default, but can be changed via instance settings to not be publicly accessible
-rw-r--r--Cargo.toml8
-rw-r--r--src/async/auth.rs46
-rw-r--r--src/async/client.rs52
-rw-r--r--src/async/mod.rs263
-rw-r--r--src/async/page.rs95
-rw-r--r--src/entities/status.rs12
-rw-r--r--src/errors.rs16
-rw-r--r--src/requests/directory.rs2
-rw-r--r--src/requests/filter.rs6
-rw-r--r--src/requests/statuses.rs175
-rw-r--r--src/requests/update_credentials.rs6
11 files changed, 538 insertions, 143 deletions
diff --git a/Cargo.toml b/Cargo.toml
index ad8b9c1..3736ba9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,11 @@ toml = { version = "0.5.0", optional = true }
tungstenite = "0.11.0"
async-trait = "0.1.40"
tokio = "0.2.22"
+async-h1 = { version = "2.1.2", optional = true }
+async-native-tls = { version = "0.3.3", optional = true }
+smol = { version = "1.2.2", optional = true }
+http-types = { version = "2.5.0", optional = true }
+async-mutex = { version = "1.4.0", optional = true }
[dependencies.chrono]
version = "0.4"
@@ -36,9 +41,10 @@ features = ["serde"]
default = ["reqwest/default-tls"]
json = []
env = ["envy"]
-all = ["toml", "json", "env"]
+all = ["toml", "json", "env", "async"]
rustls-tls = ["reqwest/rustls-tls"]
nightly = []
+async = ["async-h1", "async-native-tls", "smol", "http-types", "async-mutex"]
[dev-dependencies]
tempfile = "3.0.3"
diff --git a/src/async/auth.rs b/src/async/auth.rs
new file mode 100644
index 0000000..6f593c4
--- /dev/null
+++ b/src/async/auth.rs
@@ -0,0 +1,46 @@
+//! Authentication mechanisms for async client
+use async_mutex::Mutex;
+use std::cell::RefCell;
+
+use crate::{
+ entities::{account::Account, card::Card, context::Context, status::Status},
+ errors::{Error, Result},
+ requests::StatusesRequest,
+};
+use http_types::{Method, Request, Response};
+use hyper_old_types::header::{parsing, Link, RelationType};
+use serde::Serialize;
+use smol::{prelude::*, Async};
+use std::net::{TcpStream, ToSocketAddrs};
+use url::Url;
+
+/// strategies for authenticating mastodon requests need to implement this trait
+#[async_trait::async_trait]
+pub trait Authenticate {
+ async fn authenticate(&self, request: &mut Request) -> Result<()>;
+}
+
+/// The null-strategy, will only allow the client to call public API endpoints
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Unauthenticated;
+#[async_trait::async_trait]
+impl Authenticate for Unauthenticated {
+ async fn authenticate(&self, _: &mut Request) -> Result<()> {
+ Ok(())
+ }
+}
+
+/// Authenticates to the server via oauth
+#[derive(Debug, Clone, PartialEq)]
+pub struct OAuth {
+ client_id: String,
+ client_secret: String,
+ redirect: String,
+ token: String,
+}
+#[async_trait::async_trait]
+impl Authenticate for Mutex<RefCell<Option<OAuth>>> {
+ async fn authenticate(&self, _: &mut Request) -> Result<()> {
+ unimplemented!()
+ }
+}
diff --git a/src/async/client.rs b/src/async/client.rs
new file mode 100644
index 0000000..5081c6f
--- /dev/null
+++ b/src/async/client.rs
@@ -0,0 +1,52 @@
+use crate::{
+ entities::{account::Account, card::Card, context::Context, status::Status},
+ errors::{Error, Result},
+};
+use http_types::{Method, Request, Response};
+use hyper_old_types::header::{parsing, Link, RelationType};
+use smol::{prelude::*, Async};
+use std::net::{TcpStream, ToSocketAddrs};
+use url::Url;
+
+// taken pretty much verbatim from `smol`s example
+
+/// Sends a request and fetches the response.
+pub(super) async fn fetch(req: Request) -> Result<Response> {
+ // Figure out the host and the port.
+ let host = req
+ .url()
+ .host()
+ .ok_or_else(|| String::from("No host found"))?
+ .to_string();
+ let port = req
+ .url()
+ .port_or_known_default()
+ .ok_or_else(|| Error::Other(String::from("No port found")))?;
+
+ // Connect to the host.
+ let socket_addr = {
+ let host = host.clone();
+ smol::unblock(move || (host.as_str(), port).to_socket_addrs())
+ .await?
+ .next()
+ .ok_or_else(|| Error::Other(String::from("No socket addr")))?
+ };
+ let stream = Async::<TcpStream>::connect(socket_addr).await?;
+
+ // Send the request and wait for the response.
+ let resp = match req.url().scheme() {
+ "http" => async_h1::connect(stream, req).await?,
+ "https" => {
+ // In case of HTTPS, establish a secure TLS connection first.
+ let stream = async_native_tls::connect(&host, stream).await?;
+ async_h1::connect(stream, req).await?
+ },
+ scheme => return Err(Error::Other(format!("unsupported scheme '{}'", scheme))),
+ };
+ Ok(resp)
+}
+
+pub(super) async fn get(url: Url) -> Result<Response> {
+ let req = Request::new(Method::Get, url);
+ Ok(fetch(req).await?)
+}
diff --git a/src/async/mod.rs b/src/async/mod.rs
new file mode 100644
index 0000000..d75e131
--- /dev/null
+++ b/src/async/mod.rs
@@ -0,0 +1,263 @@
+//! Async Mastodon Client
+//!
+//! # Example
+//!
+//! ```rust,no_run
+//! use elefren::r#async::Client;
+//! use url::Url;
+//!
+//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
+//! # smol::block_on(async {
+//! let client = Client::new("https://mastodon.social")?;
+//!
+//! // iterate page-by-page
+//! // this API isn't ideal, but one day we'll get better
+//! // syntax support for iterating over streams and we can
+//! // do better
+//! let mut pages = client.public_timeline(None).await?;
+//! while let Some(statuses) = pages.next_page().await? {
+//! for status in statuses {
+//! println!("{:?}", status);
+//! }
+//! }
+//! # Ok(())
+//! # })
+//! }
+//! ```
+#![allow(warnings)]
+#![allow(missing_docs)]
+use crate::{
+ entities::{
+ account::Account,
+ activity::Activity,
+ card::Card,
+ context::Context,
+ instance::Instance,
+ poll::Poll,
+ status::{Emoji, Status, Tag},
+ },
+ errors::{Error, Result},
+ requests::{DirectoryRequest, StatusesRequest},
+};
+use http_types::{Method, Request, Response};
+use std::fmt::Debug;
+use url::Url;
+
+pub use auth::Authenticate;
+use auth::{OAuth, Unauthenticated};
+pub use page::Page;
+
+mod auth;
+mod client;
+mod page;
+
+/// Async unauthenticated client
+#[derive(Debug)]
+pub struct Client<A: Debug + Authenticate> {
+ base_url: Url,
+ auth: A,
+}
+impl Client<Unauthenticated> {
+ pub fn new<S: AsRef<str>>(base_url: S) -> Result<Client<Unauthenticated>> {
+ let base_url = Url::parse(base_url.as_ref())?;
+ Ok(Client {
+ base_url,
+ auth: Unauthenticated,
+ })
+ }
+}
+impl<A: Debug + Authenticate> Client<A> {
+ async fn send(&self, mut req: Request) -> Result<Response> {
+ self.auth.authenticate(&mut req).await?;
+ Ok(client::fetch(req).await?)
+ }
+
+ /// GET /api/v1/timelines/public
+ pub async fn public_timeline<'a, 'client: 'a, I: Into<Option<StatusesRequest<'a>>>>(
+ &'client self,
+ opts: I,
+ ) -> Result<Page<'client, Status, A>> {
+ let mut url = self.base_url.join("api/v1/timelines/public")?;
+ if let Some(opts) = opts.into() {
+ let qs = opts.to_querystring()?;
+ url.set_query(Some(&qs[..]));
+ };
+ Ok(Page::new(Request::new(Method::Get, url), &self.auth))
+ }
+
+ /// GET /api/v1/timelines/tag/:tag
+ pub async fn hashtag_timeline<'a, 'client: 'a, I: Into<Option<StatusesRequest<'a>>>>(
+ &'client self,
+ tag: &str,
+ opts: I,
+ ) -> Result<Page<'client, Status, A>> {
+ let mut url = self
+ .base_url
+ .join(&format!("api/v1/timelines/tag/{}", tag))?;
+ if let Some(opts) = opts.into() {
+ let qs = opts.to_querystring()?;
+ url.set_query(Some(&qs[..]));
+ }
+ Ok(Page::new(Request::new(Method::Get, url), &self.auth))
+ }
+
+ /// GET /api/v1/statuses/:id
+ pub async fn status(&self, id: &str) -> Result<Status> {
+ let url = self.base_url.join(&format!("api/v1/statuses/{}", id))?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/statuses/:id/context
+ pub async fn context(&self, id: &str) -> Result<Context> {
+ let url = self
+ .base_url
+ .join(&format!("api/v1/statuses/{}/context", id))?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/statuses/:id/card
+ pub async fn card(&self, id: &str) -> Result<Card> {
+ let url = self
+ .base_url
+ .join(&format!("api/v1/statuses/{}/card", id))?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/statuses/:id/reblogged_by
+ pub async fn reblogged_by<'client>(
+ &'client self,
+ id: &str,
+ ) -> Result<Page<'client, Account, A>> {
+ let url = self
+ .base_url
+ .join(&format!("api/v1/statuses/{}/reblogged_by", id))?;
+ Ok(Page::new(Request::new(Method::Get, url), &self.auth))
+ }
+
+ /// GET /api/v1/statuses/:id/favourited_by
+ pub async fn favourited_by<'client>(
+ &'client self,
+ id: &str,
+ ) -> Result<Page<'client, Account, A>> {
+ let url = self
+ .base_url
+ .join(&format!("api/v1/statuses/{}/favourited_by", id))?;
+ Ok(Page::new(Request::new(Method::Get, url), &self.auth))
+ }
+
+ /// GET /api/v1/accounts/:id
+ pub async fn account(&self, id: &str) -> Result<Account> {
+ let url = self.base_url.join(&format!("api/v1/accounts/{}", id))?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/accounts/:id/statuses
+ pub async fn account_statuses<'a, 'client: 'a, I: Into<Option<StatusesRequest<'a>>>>(
+ &'client self,
+ id: &str,
+ request: I,
+ ) -> Result<Page<'client, Status, A>> {
+ let mut url = self
+ .base_url
+ .join(&format!("api/v1/accounts/{}/statuses", id))?;
+ if let Some(request) = request.into() {
+ let qs = request.to_querystring()?;
+ url.set_query(Some(&qs[..]));
+ }
+ Ok(Page::new(Request::new(Method::Get, url), &self.auth))
+ }
+
+ /// GET /api/v1/polls/:id
+ pub async fn poll(&self, id: &str) -> Result<Poll> {
+ let url = self.base_url.join(&format!("api/v1/polls/{}", id))?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/instance
+ pub async fn instance(&self) -> Result<Instance> {
+ let url = self.base_url.join("api/v1/instance")?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/instance/peers
+ pub async fn peers(&self) -> Result<Vec<String>> {
+ let url = self.base_url.join("api/v1/instance/peers")?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/instance/activity
+ pub async fn activity(&self) -> Result<Option<Vec<Activity>>> {
+ let url = self.base_url.join("api/v1/instance/activity")?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/custom_emojis
+ pub async fn custom_emojis(&self) -> Result<Vec<Emoji>> {
+ let url = self.base_url.join("api/v1/custom_emojis")?;
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/directory
+ pub async fn directory<'a, I: Into<Option<DirectoryRequest<'a>>>>(
+ &self,
+ opts: I,
+ ) -> Result<Vec<Account>> {
+ let mut url = self.base_url.join("api/v1/directory")?;
+ if let Some(opts) = opts.into() {
+ let qs = opts.to_querystring()?;
+ url.set_query(Some(&qs[..]));
+ }
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+
+ /// GET /api/v1/trends
+ pub async fn trends<I: Into<Option<usize>>>(&self, limit: I) -> Result<Vec<Tag>> {
+ let mut url = self.base_url.join("api/v1/trends")?;
+ if let Some(limit) = limit.into() {
+ url.set_query(Some(&format!("?limit={}", limit)));
+ }
+ let response = self.send(Request::new(Method::Get, url)).await?;
+ Ok(deserialize(response).await?)
+ }
+}
+
+async fn deserialize<T: serde::de::DeserializeOwned>(mut response: Response) -> Result<T> {
+ let status = response.status();
+ if status.is_client_error() {
+ // TODO
+ // return Err(Error::Client(status));
+ return Err(Error::Other(String::from("4xx status code")));
+ } else if status.is_server_error() {
+ // TODO
+ // return Err(Error::Server(status)) // TODO
+ return Err(Error::Other(String::from("5xx status code")));
+ } else if status.is_redirection() || status.is_informational() {
+ return Err(Error::Other(String::from("3xx or 1xx status code")));
+ }
+ let bytes = response.body_bytes().await?;
+ Ok(match serde_json::from_slice::<T>(&bytes) {
+ Ok(t) => {
+ log::debug!("{}", String::from_utf8_lossy(&bytes));
+ t
+ },
+ Err(e) => {
+ log::error!("{}", String::from_utf8_lossy(&bytes));
+ let err = if let Ok(error) = serde_json::from_slice(&bytes) {
+ Error::Api(error)
+ } else {
+ e.into()
+ };
+ return Err(err);
+ },
+ })
+}
diff --git a/src/async/page.rs b/src/async/page.rs
new file mode 100644
index 0000000..4ef4161
--- /dev/null
+++ b/src/async/page.rs
@@ -0,0 +1,95 @@
+use super::{client, deserialize, Authenticate};
+use crate::{
+ entities::{account::Account, card::Card, context::Context, status::Status},
+ errors::{Error, Result},
+};
+use http_types::{Method, Request, Response};
+use hyper_old_types::header::{parsing, Link, RelationType};
+use smol::{prelude::*, Async};
+use std::{
+ fmt::Debug,
+ net::{TcpStream, ToSocketAddrs},
+};
+use url::Url;
+
+// link header name
+const LINK: &str = "link";
+
+#[derive(Debug)]
+pub struct Page<'client, T, A: Authenticate + Debug + 'client> {
+ next: Option<Request>,
+ prev: Option<Request>,
+ auth: &'client A,
+ _marker: std::marker::PhantomData<T>,
+}
+impl<'client, T: serde::de::DeserializeOwned, A: Authenticate + Debug + 'client>
+ Page<'client, T, A>
+{
+ pub fn new(next: Request, auth: &'client A) -> Page<'client, T, A> {
+ Page {
+ next: Some(next),
+ prev: None,
+ auth,
+ _marker: std::marker::PhantomData,
+ }
+ }
+
+ pub async fn next_page(&mut self) -> Result<Option<Vec<T>>> {
+ let mut req = if let Some(next) = self.next.take() {
+ next
+ } else {
+ return Ok(None);
+ };
+ Ok(self.send(req).await?)
+ }
+
+ pub async fn prev_page(&mut self) -> Result<Option<Vec<T>>> {
+ let req = if let Some(prev) = self.prev.take() {
+ prev
+ } else {
+ return Ok(None);
+ };
+ Ok(self.send(req).await?)
+ }
+
+ async fn send(&mut self, mut req: Request) -> Result<Option<Vec<T>>> {
+ self.auth.authenticate(&mut req).await?;
+ log::trace!("Request: {:?}", req);
+ let response = client::fetch(req).await?;
+ log::trace!("Response: {:?}", response);
+ self.fill_links_from_resp(&response)?;
+ let items = deserialize(response).await?;
+ Ok(items)
+ }
+
+ fn fill_links_from_resp(&mut self, response: &Response) -> Result<()> {
+ let (prev, next) = get_links(&response)?;
+ self.prev = prev.map(|url| Request::new(Method::Get, url));
+ self.next = next.map(|url| Request::new(Method::Get, url));
+ Ok(())
+ }
+}
+
+fn get_links(response: &Response) -> Result<(Option<Url>, Option<Url>)> {
+ let mut prev = None;
+ let mut next = None;
+
+ if let Some(link_header) = response.header(LINK) {
+ let link_header = link_header.as_str();
+ let link_header = link_header.as_bytes();
+ let link_header: Link = parsing::from_raw_str(&link_header)?;
+ for value in link_header.values() {
+ if let Some(relations) = value.rel() {
+ if relations.contains(&RelationType::Next) {
+ next = Some(Url::parse(value.link())?);
+ }
+
+ if relations.contains(&RelationType::Prev) {
+ prev = Some(Url::parse(value.link())?);
+ }
+ }
+ }
+ }
+
+ Ok((prev, next))
+}
diff --git a/src/entities/status.rs b/src/entities/status.rs
index 7243449..c65cb9c 100644
--- a/src/entities/status.rs
+++ b/src/entities/status.rs
@@ -2,11 +2,8 @@
use super::prelude::*;
use crate::{
- entities::{
- card::Card,
- poll::Poll,
- },
- status_builder::Visibility
+ entities::{card::Card, poll::Poll},
+ status_builder::Visibility,
};
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
@@ -64,7 +61,10 @@ pub struct Status {
pub card: Option<Card>,
/// The detected language for the status, if detected.
pub language: Option<String>,
- /// Plain-text source of a status. Returned instead of content when status is deleted, so the user may redraft from the source text without the client having to reverse-engineer the original text from the HTML content.
+ /// Plain-text source of a status. Returned instead of content when status
+ /// is deleted, so the user may redraft from the source text without the
+ /// client having to reverse-engineer the original text from the HTML
+ /// content.
pub text: Option<String>,
/// Whether the application client has favourited the status.
pub favourited: Option<bool>,
diff --git a/src/errors.rs b/src/errors.rs
index 2813bfd..6b58d05 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -5,8 +5,12 @@ use std::{error, fmt, io::Error as IoError};
use ::toml::de::Error as TomlDeError;
#[cfg(feature = "toml")]
use ::toml::ser::Error as TomlSerError;
+#[cfg(feature = "async")]
+use async_native_tls::Error as TlsError;
#[cfg(feature = "env")]
use envy::Error as EnvyError;
+#[cfg(feature = "async")]
+use http_types::Error as HttpTypesError;
use hyper_old_types::Error as HeaderParseError;
use reqwest::{header::ToStrError as HeaderStrError, Error as HttpError, StatusCode};
use serde_json::Error as SerdeError;
@@ -64,6 +68,12 @@ pub enum Error {
SerdeQs(SerdeQsError),
/// WebSocket error
WebSocket(WebSocketError),
+ #[cfg(feature = "async")]
+ /// http-types error
+ HttpTypes(HttpTypesError),
+ #[cfg(feature = "async")]
+ /// TLS error
+ Tls(TlsError),
/// Other errors
Other(String),
}
@@ -99,6 +109,10 @@ impl error::Error for Error {
Error::ClientSecretRequired => return None,
Error::AccessTokenRequired => return None,
Error::MissingField(_) => return None,
+ #[cfg(feature = "async")]
+ Error::HttpTypes(..) => return None,
+ #[cfg(feature = "async")]
+ Error::Tls(ref e) => e,
Error::Other(..) => return None,
})
}
@@ -149,6 +163,8 @@ from! {
#[cfg(feature = "env")] EnvyError, Envy,
SerdeQsError, SerdeQs,
WebSocketError, WebSocket,
+ #[cfg(feature = "async")] HttpTypesError, HttpTypes,
+ #[cfg(feature = "async")] TlsError, Tls,
String, Other,
}
diff --git a/src/requests/directory.rs b/src/requests/directory.rs
index 14ff2c4..f410e52 100644
--- a/src/requests/directory.rs
+++ b/src/requests/directory.rs
@@ -75,6 +75,6 @@ impl<'a> DirectoryRequest<'a> {
/// );
/// ```
pub fn to_querystring(&self) -> Result<String, Error> {
- Ok(format!("{}", serde_qs::to_string(&self)?))
+ Ok(serde_qs::to_string(&self)?)
}
}
diff --git a/src/requests/filter.rs b/src/requests/filter.rs
index 7156243..448a18f 100644
--- a/src/requests/filter.rs
+++ b/src/requests/filter.rs
@@ -126,7 +126,8 @@ mod tests {
#[test]
fn test_expires_in() {
- let request = AddFilterRequest::new("foo", FilterContext::Home).expires_in(Duration::from_secs(300));
+ let request =
+ AddFilterRequest::new("foo", FilterContext::Home).expires_in(Duration::from_secs(300));
assert_eq!(
request,
AddFilterRequest {
@@ -141,7 +142,8 @@ mod tests {
#[test]
fn test_serialize_request() {
- let request = AddFilterRequest::new("foo", FilterContext::Home).expires_in(Duration::from_secs(300));
+ let request =
+ AddFilterRequest::new("foo", FilterContext::Home).expires_in(Duration::from_secs(300));
let ser = serde_json::to_string(&request).expect("Couldn't serialize");
assert_eq!(
ser,
diff --git a/src/requests/statuses.rs b/src/requests/statuses.rs
index 87baac6..79124a5 100644
--- a/src/requests/statuses.rs
+++ b/src/requests/statuses.rs
@@ -412,230 +412,143 @@ mod tests {
#[test]
fn test_to_querystring() {
macro_rules! qs_test {
- (|$r:ident| $b:block, $expected:expr) => {
- {
- let $r = StatusesRequest::new();
- let $r = $b;
- let qs = $r.to_querystring().expect("Failed to serialize querystring");
- assert_eq!(&qs, $expected);
- }
- }
+ (| $r:ident | $b:block, $expected:expr) => {{
+ let $r = StatusesRequest::new();
+ let $r = $b;
+ let qs = $r
+ .to_querystring()
+ .expect("Failed to serialize querystring");
+ assert_eq!(&qs, $expected);
+ }};
}
+ qs_test!(|request| { request.only_media() }, "?only_media=1");
qs_test!(
- |request| {
- request.only_media()
- },
- "?only_media=1"
- );
- qs_test!(
- |request| {
- request.exclude_replies()
- },
+ |request| { request.exclude_replies() },
"?exclude_replies=1"
);
+ qs_test!(|request| { request.pinned() }, "?pinned=1");
+ qs_test!(|request| { request.max_id("foo") }, "?max_id=foo");
+ qs_test!(|request| { request.since_id("foo") }, "?since_id=foo");
+ qs_test!(|request| { request.limit(42) }, "?limit=42");
qs_test!(
- |request| {
- request.pinned()
- },
- "?pinned=1"
- );
- qs_test!(
- |request| {
- request.max_id("foo")
- },
- "?max_id=foo"
- );
- qs_test!(
- |request| {
- request.since_id("foo")
- },
- "?since_id=foo"
- );
- qs_test!(
- |request| {
- request.limit(42)
- },
- "?limit=42"
- );
- qs_test!(
- |request| {
- request.only_media().exclude_replies()
- },
+ |request| { request.only_media().exclude_replies() },
"?only_media=1&exclude_replies=1"
);
qs_test!(
- |request| {
- request.only_media().pinned()
- },
+ |request| { request.only_media().pinned() },
"?only_media=1&pinned=1"
);
qs_test!(
- |request| {
- request.only_media().max_id("foo")
- },
+ |request| { request.only_media().max_id("foo") },
"?only_media=1&max_id=foo"
);
qs_test!(
- |request| {
- request.only_media().since_id("foo")
- },
+ |request| { request.only_media().since_id("foo") },
"?only_media=1&since_id=foo"
);
qs_test!(
- |request| {
- request.only_media().limit(42)
- },
+ |request| { request.only_media().limit(42) },
"?only_media=1&limit=42"
);
qs_test!(
- |request| {
- request.exclude_replies().only_media()
- },
+ |request| { request.exclude_replies().only_media() },
"?only_media=1&exclude_replies=1"
);
qs_test!(
- |request| {
- request.exclude_replies().pinned()
- },
+ |request| { request.exclude_replies().pinned() },
"?exclude_replies=1&pinned=1"
);
qs_test!(
- |request| {
- request.exclude_replies().max_id("foo")
- },
+ |request| { request.exclude_replies().max_id("foo") },
"?exclude_replies=1&max_id=foo"
);
qs_test!(
- |request| {
- request.exclude_replies().since_id("foo")
- },
+ |request| { request.exclude_replies().since_id("foo") },
"?exclude_replies=1&since_id=foo"
);
qs_test!(
- |request| {
- request.exclude_replies().limit(42)
- },
+ |request| { request.exclude_replies().limit(42) },
"?exclude_replies=1&limit=42"
);
qs_test!(
- |request| {
- request.pinned().only_media()
- },
+ |request| { request.pinned().only_media() },
"?only_media=1&pinned=1"
);
qs_test!(
- |request| {
- request.pinned().exclude_replies()
- },
+ |request| { request.pinned().exclude_replies() },
"?exclude_replies=1&pinned=1"
);
qs_test!(
- |request| {
- request.pinned().max_id("foo")
- },
+ |request| { request.pinned().max_id("foo") },
"?pinned=1&max_id=foo"
);
qs_test!(
- |request| {
- request.pinned().since_id("foo")
- },
+ |request| { request.pinned().since_id("foo") },
"?pinned=1&since_id=foo"
);
qs_test!(
- |request| {
- request.pinned().limit(42)
- },
+ |request| { request.pinned().limit(42) },
"?pinned=1&limit=42"
);
qs_test!(
- |request| {
- request.max_id("foo").only_media()
- },
+ |request| { request.max_id("foo").only_media() },
"?only_media=1&max_id=foo"
);
qs_test!(
- |request| {
- request.max_id("foo").exclude_replies()
- },
+ |request| { request.max_id("foo").exclude_replies() },
"?exclude_replies=1&max_id=foo"
);
qs_test!(
- |request| {
- request.max_id("foo").pinned()
- },
+ |request| { request.max_id("foo").pinned() },
"?pinned=1&max_id=foo"
);
qs_test!(
- |request| {
- request.max_id("foo").since_id("foo")
- },
+ |request| { request.max_id("foo").since_id("foo") },
"?max_id=foo&since_id=foo"
);
qs_test!(
- |request| {
- request.max_id("foo").limit(42)
- },
+ |request| { request.max_id("foo").limit(42) },
"?max_id=foo&limit=42"
);
qs_test!(
- |request| {
- request.since_id("foo").only_media()
- },
+ |request| { request.since_id("foo").only_media() },
"?only_media=1&since_id=foo"
);
qs_test!(
- |request| {
- request.since_id("foo").exclude_replies()
- },
+ |request| { request.since_id("foo").exclude_replies() },
"?exclude_replies=1&since_id=foo"
);
qs_test!(
- |request| {
- request.since_id("foo").pinned()
- },
+ |request| { request.since_id("foo").pinned() },
"?pinned=1&since_id=foo"
);
qs_test!(
- |request| {
- request.since_id("foo").max_id("foo")
- },
+ |request| { request.since_id("foo").max_id("foo") },
"?max_id=foo&since_id=foo"
);
qs_test!(
- |request| {
- request.since_id("foo").limit(42)
- },
+ |request| { request.since_id("foo").limit(42) },
"?since_id=foo&limit=42"
);
qs_test!(
- |request| {
- request.limit(42).only_media()
- },
+ |request| { request.limit(42).only_media() },
"?only_media=1&limit=42"
);
qs_test!(
- |request| {
- request.limit(42).exclude_replies()
- },
+ |request| { request.limit(42).exclude_replies() },
"?exclude_replies=1&limit=42"
);
qs_test!(
- |request| {
- request.limit(42).pinned()
- },
+ |request| { request.limit(42).pinned() },
"?pinned=1&limit=42"
);
qs_test!(
- |request| {
- request.limit(42).max_id("foo")
- },
+ |request| { request.limit(42).max_id("foo") },
"?max_id=foo&limit=42"
);
qs_test!(
- |request| {
- request.limit(42).since_id("foo")
- },
+ |request| { request.limit(42).since_id("foo") },
"?since_id=foo&limit=42"
);
}
diff --git a/src/requests/update_credentials.rs b/src/requests/update_credentials.rs
index 18f279d..325eafd 100644
--- a/src/requests/update_credentials.rs
+++ b/src/requests/update_credentials.rs
@@ -195,7 +195,7 @@ impl UpdateCredsRequest {
privacy: self.privacy,
sensitive: self.sensitive,
}),
- fields_attributes: self.field_attributes.clone(),
+ fields_attributes: self.field_attributes,
})
}
}
@@ -305,7 +305,9 @@ mod tests {
#[test]
fn test_update_creds_request_build() {
- let builder = UpdateCredsRequest::new().display_name("test").note("a note");
+ let builder = UpdateCredsRequest::new()
+ .display_name("test")
+ .note("a note");
let creds = builder.build().expect("Couldn't build Credentials");
assert_eq!(
creds,