Skip to content

Commit

Permalink
use thiserror for error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
adwhit committed Mar 14, 2020
1 parent 23de9d3 commit c2b556f
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 107 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ regex = "1.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha-1 = "0.8"
thiserror = "1.0.11"
tokio = { version = "0.2.8", features = ["time", "rt-core", "macros"] }
url = "2.1.1"

Expand Down
11 changes: 9 additions & 2 deletions src/common/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ use serde_json;

use std::convert::TryFrom;
use std::future::Future;
use std::iter::FromIterator;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{io, mem, slice, vec};
use std::{io, mem};

use super::Headers;

Expand Down Expand Up @@ -66,6 +65,7 @@ fn rate_limit_reset(headers: &Headers) -> Result<Option<i32>> {
///methods as if they were methods on this struct.
#[derive(Debug, Deserialize, derive_more::Constructor)]
pub struct Response<T> {
/// Latest rate lime status
pub rate_limit_status: RateLimit,
///The decoded response from the request.
pub response: T,
Expand Down Expand Up @@ -287,6 +287,13 @@ pub async fn make_future<T>(
.await
}

/// Shortcut function to create a `TwitterFuture` that parses out the given type from its response.
pub async fn make_parsed_future<T: for<'de> Deserialize<'de>>(
request: Request<Body>,
) -> Result<Response<T>> {
make_future(request, make_response).await
}

#[derive(Clone, Debug, Deserialize)]
pub struct RateLimit {
///The rate limit ceiling for the given request.
Expand Down
2 changes: 1 addition & 1 deletion src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use serde::{de::DeserializeOwned, Deserialize};

use crate::common::*;
use crate::error::Result;
use crate::{auth, error, list, user};
use crate::{auth, list, user};

///Trait to generalize over paginated views of API results.
///
Expand Down
108 changes: 30 additions & 78 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub(crate) type Result<T> = std::result::Result<T, Error>;
///This is returned as part of [`Error::TwitterError`][] whenever Twitter has rejected a call.
///
///[`Error::TwitterError`]: enum.Error.html
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, thiserror::Error)]
pub struct TwitterErrors {
///A collection of errors returned by Twitter.
pub errors: Vec<TwitterErrorCode>,
Expand Down Expand Up @@ -74,7 +74,8 @@ impl fmt::Display for TwitterErrorCode {
}

/// Represents an error that can occur during media processing.
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[derive(Debug, Clone, PartialEq, Deserialize, thiserror::Error)]
#[error("Media error {code} ({name}) - {message}")]
pub struct MediaError {
/// A numeric error code assigned to the error.
pub code: i32,
Expand All @@ -85,135 +86,86 @@ pub struct MediaError {
}

/// A set of errors that can occur when interacting with Twitter.
#[derive(Debug, derive_more::From)]
#[derive(Debug, thiserror::Error)]
pub enum Error {
///A URL was passed to a shortcut function that didn't match the method being called.
#[error("URL given did not match API method")]
BadUrl,
///The response from Twitter was formatted incorrectly or in an unexpected manner. The enclosed
///values are an explanatory string and, if applicable, the input that caused the error.
///
///This usually reflects a bug in this library, as it means I'm not parsing input right.
#[error("Invalid response received: {} ({:?})", _0, _1)]
InvalidResponse(&'static str, Option<String>),
///The response from Twitter was missing an expected value. The enclosed value was the
///expected parameter.
///
///This usually reflects a bug in this library, as it means I'm expecting a value that may not
///always be there, and need to update my parsing to reflect this.
#[error("Value missing from response: {}", _0)]
MissingValue(&'static str),
///The `Future` being polled has already returned a completed value (or another error). In
///order to retry the request, create the `Future` again.
#[error("Future has already completed")]
FutureAlreadyCompleted,
///The response from Twitter returned an error structure instead of the expected response. The
///enclosed value was the response from Twitter.
TwitterError(TwitterErrors),
#[error("Errors returned by Twitter: {}", _0)]
TwitterError(#[from] TwitterErrors),
///The response returned from Twitter contained an error indicating that the rate limit for
///that method has been reached. The enclosed value is the Unix timestamp in UTC when the next
///rate-limit window will open.
#[error("Rate limit reached, hold until {}", _0)]
RateLimit(i32),
///An attempt to upload a video or gif successfully uploaded the file, but failed in
///post-processing. The enclosed value contains the error message from Twitter.
MediaError(MediaError),
#[error("Error processing media: {}", _0)]
MediaError(#[from] MediaError),
///The response from Twitter gave a response code that indicated an error. The enclosed value
///was the response code.
///
///This is only returned if Twitter did not also return an [error code][TwitterErrors] in the
///response body. That check is performed before examining the status code.
///
///[TwitterErrors]: struct.TwitterErrors.html
#[error("Error status received: {}", _0)]
BadStatus(hyper::StatusCode),
///The web request experienced an error. The enclosed error was returned from hyper.
NetError(hyper::error::Error),
#[error("Network error: {}", _0)]
NetError(#[from] hyper::error::Error),
///The `native_tls` implementation returned an error. The enclosed error was returned from
///`native_tls`.
#[cfg(feature = "native_tls")]
TlsError(native_tls::Error),
#[error("TLS error: {}", _0)]
TlsError(#[from] native_tls::Error),
///An error was experienced while processing the response stream. The enclosed error was
///returned from libstd.
IOError(std::io::Error),
#[error("IO error: {}", _0)]
IOError(#[from] std::io::Error),
///An error occurred while loading the JSON response. The enclosed error was returned from
///`serde_json`.
DeserializeError(serde_json::Error),
#[error("JSON deserialize error: {}", _0)]
DeserializeError(#[from] serde_json::Error),
///An error occurred when parsing a timestamp from Twitter. The enclosed error was returned
///from chrono.
TimestampParseError(chrono::ParseError),
#[error("Error parsing timestamp: {}", _0)]
TimestampParseError(#[from] chrono::ParseError),
///The tokio `Timer` instance was shut down while waiting on a timer, for example while waiting
///for media to be processed by Twitter. The enclosed error was returned from `tokio`.
TimerShutdownError(tokio::time::Error),
#[error("Timer runtime shutdown: {}", _0)]
TimerShutdownError(#[from] tokio::time::Error),
///An error occurred when reading the value from a response header. The enclused error was
///returned from hyper.
///
///This error should be considerably rare, but is included to ensure that egg-mode doesn't
///panic if it receives malformed headers or the like.
HeaderParseError(hyper::header::ToStrError),
#[error("Error decoding headers: {}", _0)]
HeaderParseError(#[from] hyper::header::ToStrError),
///An error occurred when converting a rate-limit header to an integer. The enclosed error was
///returned from the standard library.
///
///This error should be considerably rare, but is included to ensure that egg-mode doesn't
///panic if it receives malformed headers or the like.
HeaderConvertError(std::num::ParseIntError),
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Error::BadUrl => write!(f, "URL given did not match API method"),
Error::InvalidResponse(err, ref ext) => {
write!(f, "Invalid response received: {} ({:?})", err, ext)
}
Error::MissingValue(val) => write!(f, "Value missing from response: {}", val),
Error::FutureAlreadyCompleted => write!(f, "Future has already been completed"),
Error::TwitterError(ref err) => write!(f, "Error(s) returned from Twitter: {}", err),
Error::RateLimit(ts) => write!(f, "Rate limit reached, hold until {}", ts),
Error::MediaError(ref err) => write!(f, "Error processing media: {}", err.message),
Error::BadStatus(ref val) => write!(f, "Error status received: {}", val),
Error::NetError(ref err) => write!(f, "Network error: {}", err),
#[cfg(feature = "native_tls")]
Error::TlsError(ref err) => write!(f, "TLS error: {}", err),
Error::IOError(ref err) => write!(f, "IO error: {}", err),
Error::DeserializeError(ref err) => write!(f, "JSON deserialize error: {}", err),
Error::TimestampParseError(ref err) => write!(f, "Error parsing timestamp: {}", err),
Error::TimerShutdownError(ref err) => write!(f, "Timer runtime shutdown: {}", err),
Error::HeaderParseError(ref err) => write!(f, "Error decoding header: {}", err),
Error::HeaderConvertError(ref err) => write!(f, "Error converting header: {}", err),
}
}
}

impl std::error::Error for Error {
fn description(&self) -> &str {
match *self {
Error::BadUrl => "URL given did not match API method",
Error::InvalidResponse(_, _) => "Invalid response received",
Error::MissingValue(_) => "Value missing from response",
Error::FutureAlreadyCompleted => "Future has already been completed",
Error::TwitterError(_) => "Error returned from Twitter",
Error::RateLimit(_) => "Rate limit for method reached",
Error::MediaError(_) => "Error processing media",
Error::BadStatus(_) => "Response included error code",
Error::NetError(ref err) => err.description(),
#[cfg(feature = "native_tls")]
Error::TlsError(ref err) => err.description(),
Error::IOError(ref err) => err.description(),
Error::DeserializeError(ref err) => err.description(),
Error::TimestampParseError(ref err) => err.description(),
Error::TimerShutdownError(ref err) => err.description(),
Error::HeaderParseError(ref err) => err.description(),
Error::HeaderConvertError(ref err) => err.description(),
}
}

fn cause(&self) -> Option<&dyn std::error::Error> {
match *self {
Error::NetError(ref err) => Some(err),
#[cfg(feature = "native_tls")]
Error::TlsError(ref err) => Some(err),
Error::IOError(ref err) => Some(err),
Error::TimestampParseError(ref err) => Some(err),
Error::DeserializeError(ref err) => Some(err),
Error::TimerShutdownError(ref err) => Some(err),
Error::HeaderParseError(ref err) => Some(err),
Error::HeaderConvertError(ref err) => Some(err),
_ => None,
}
}
#[error("Error converting headers: {}", _0)]
HeaderConvertError(#[from] std::num::ParseIntError),
}
2 changes: 1 addition & 1 deletion src/media/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,10 @@ pub struct MediaHandle {
}

impl MediaHandle {
#[inline]
/// Returns whether media is still valid to be used in API calls.
///
/// Under hood it is `Instant::now() < handle.valid_until`.
#[inline]
pub fn is_valid(&self) -> bool {
Instant::now() < self.valid_until
}
Expand Down
41 changes: 20 additions & 21 deletions src/place/fun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use crate::common::*;
use crate::error::{Error::BadUrl, Result};
use crate::error::{Error, Result};
use crate::{auth, links};

use super::PlaceQuery;
Expand Down Expand Up @@ -52,28 +52,27 @@ pub fn reverse_geocode(latitude: f64, longitude: f64) -> GeocodeBuilder {
GeocodeBuilder::new(latitude, longitude)
}

fn parse_url<'a>(base: &'static str, full: &'a str) -> Result<ParamList> {
// let mut iter = full.split('?');
fn parse_url(base: &'static str, full: &str) -> Result<ParamList> {
let mut iter = full.split('?');

// if let Some(base_part) = iter.next() {
// if base_part != base {
// return Err(BadUrl);
// }
// } else {
// return Err(BadUrl);
// }
if let Some(base_part) = iter.next() {
if base_part != base {
return Err(Error::BadUrl);
}
} else {
return Err(Error::BadUrl);
}

// if let Some(list) = iter.next() {
// list.split('&').try_fold(ParamList::new(), |p, pair| {
// let mut kv_iter = pair.split('=');
// let k = kv_iter.next().ok_or(BadUrl)?;
// let v = kv_iter.next().ok_or(BadUrl)?;
// Ok(p.add_param(k, v))
// })
// } else {
// Err(BadUrl)
// }
todo!()
if let Some(list) = iter.next() {
list.split('&').try_fold(ParamList::new(), |p, pair| {
let mut kv_iter = pair.split('=');
let k = kv_iter.next().ok_or(Error::BadUrl)?;
let v = kv_iter.next().ok_or(Error::BadUrl)?;
Ok(p.add_param(k.to_string(), v.to_string()))
})
} else {
Err(Error::BadUrl)
}
}

///From a URL given with the result of `reverse_geocode`, perform the same reverse-geocode search.
Expand Down
1 change: 0 additions & 1 deletion src/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
//! [search-doc]: https://dev.twitter.com/rest/public/search
//! [search-place]: https://dev.twitter.com/rest/public/search-by-place
use std::borrow::Cow;
use std::fmt;

use serde::{Deserialize, Deserializer};
Expand Down
2 changes: 0 additions & 2 deletions src/user/fun.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ use crate::common::*;
use crate::error::Result;
use crate::{auth, cursor, links};

use std::borrow::Cow;

use super::*;

//---Groups of users---
Expand Down
1 change: 0 additions & 1 deletion src/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
//! - `mutes`/`mutes_ids`
//! - `incoming_requests`/`outgoing_requests`
use std::borrow::Cow;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
Expand Down

0 comments on commit c2b556f

Please sign in to comment.