Skip to content

Commit

Permalink
Merge pull request #238 from threema-donat/switch-to-thiserror
Browse files Browse the repository at this point in the history
Use thiserror instead of anyhow
  • Loading branch information
dermesser authored Jul 2, 2024
2 parents 732f25d + e0daa91 commit d62c641
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 104 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ hyper-tls = ["dep:hyper-tls", "__rustls"]
__rustls = ["dep:rustls"]

[dependencies]
anyhow = "1.0.38"
async-trait = "^0.1"
base64 = "0.22"
futures = "0.3"
Expand All @@ -55,6 +54,7 @@ rustls-pemfile = { version = "2.0.0", optional = true }
seahash = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.61"
time = { version = "0.3.7", features = ["local-offset", "parsing", "serde"] }
tokio = { version = "1.0", features = [ "fs", "macros", "io-std", "io-util", "time", "sync", "rt"] }
url = "2"
Expand Down
18 changes: 9 additions & 9 deletions examples/custom_storage.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
//! Demonstrating how to create a custom token store
use anyhow::anyhow;
use async_trait::async_trait;
use std::sync::RwLock;
use yup_oauth2::storage::{TokenInfo, TokenStorage};
use std::{borrow::Cow, sync::RwLock};
use yup_oauth2::storage::{TokenInfo, TokenStorage, TokenStorageError};

struct ExampleTokenStore {
store: RwLock<Vec<StoredToken>>,
Expand All @@ -25,15 +24,16 @@ fn scopes_covered_by(scopes: &[&str], possible_match_or_superset: &[&str]) -> bo
/// to disk, an OS keychain, a database or whatever suits your use-case
#[async_trait]
impl TokenStorage for ExampleTokenStore {
async fn set(&self, scopes: &[&str], token: TokenInfo) -> anyhow::Result<()> {
let data = serde_json::to_string(&token).unwrap();
async fn set(&self, scopes: &[&str], token: TokenInfo) -> Result<(), TokenStorageError> {
let data = serde_json::to_string(&token).map_err(|e| {
TokenStorageError::Other(Cow::Owned(format!("Failed to parse JSON: {e}")))
})?;

println!("Storing token for scopes {:?}", scopes);

let mut store = self
.store
.write()
.map_err(|_| anyhow!("Unable to lock store for writing"))?;
let mut store = self.store.write().map_err(|_| {
TokenStorageError::Other(Cow::Borrowed("Unable to lock store for writing"))
})?;

store.push(StoredToken {
scopes: scopes.iter().map(|str| str.to_string()).collect(),
Expand Down
113 changes: 25 additions & 88 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ use std::error::Error as StdError;
use std::fmt;
use std::io;

use hyper::Error as HyperError;
use hyper_util::client::legacy::Error as LegacyHyperError;
use serde::Deserialize;
use thiserror::Error as ThisError;

pub use crate::external_account::CredentialSourceError;
pub use crate::storage::TokenStorageError;

/// Error returned by the authorization server.
///
Expand Down Expand Up @@ -142,104 +148,35 @@ impl<T> AuthErrorOr<T> {
}

/// Encapsulates all possible results of the `token(...)` operation
#[derive(Debug)]
#[derive(Debug, ThisError)]
pub enum Error {
/// Indicates connection failure
HttpError(hyper::Error),
#[error("Connection failure: {0}")]
HttpError(#[from] HyperError),
/// Indicates connection failure
HttpClientError(hyper_util::client::legacy::Error),
#[error("Connection failure: {0}")]
HttpClientError(#[from] LegacyHyperError),
/// The server returned an error.
AuthError(AuthError),
#[error("Server error: {0}")]
AuthError(#[from] AuthError),
/// Error while decoding a JSON response.
JSONError(serde_json::Error),
#[error("JSON Error; this might be a bug with unexpected server responses! {0}")]
JSONError(#[from] serde_json::Error),
/// Error within user input.
#[error("Invalid user input: {0}")]
UserError(String),
/// A lower level IO error.
LowLevelError(io::Error),
#[error("Low level error: {0}")]
LowLevelError(#[from] io::Error),
/// We required an access token, but received a response that didn't contain one.
#[error("Expected an access token, but received a response without one")]
MissingAccessToken,
/// Other errors produced by a storage provider
OtherError(anyhow::Error),
}

impl From<hyper::Error> for Error {
fn from(error: hyper::Error) -> Error {
Error::HttpError(error)
}
}

impl From<hyper_util::client::legacy::Error> for Error {
fn from(error: hyper_util::client::legacy::Error) -> Error {
Error::HttpClientError(error)
}
}

impl From<AuthError> for Error {
fn from(value: AuthError) -> Error {
Error::AuthError(value)
}
}

impl From<serde_json::Error> for Error {
fn from(value: serde_json::Error) -> Error {
Error::JSONError(value)
}
}

impl From<io::Error> for Error {
fn from(value: io::Error) -> Error {
Error::LowLevelError(value)
}
}

impl From<anyhow::Error> for Error {
fn from(value: anyhow::Error) -> Error {
match value.downcast::<io::Error>() {
Ok(io_error) => Error::LowLevelError(io_error),
Err(err) => Error::OtherError(err),
}
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Error::HttpError(ref err) => err.fmt(f),
Error::HttpClientError(ref err) => err.fmt(f),
Error::AuthError(ref err) => err.fmt(f),
Error::JSONError(ref e) => {
write!(
f,
"JSON Error; this might be a bug with unexpected server responses! {}",
e
)?;
Ok(())
}
Error::UserError(ref s) => s.fmt(f),
Error::LowLevelError(ref e) => e.fmt(f),
Error::MissingAccessToken => {
write!(
f,
"Expected an access token, but received a response without one"
)?;
Ok(())
}
Error::OtherError(ref e) => e.fmt(f),
}
}
}

impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match *self {
Error::HttpError(ref err) => Some(err),
Error::HttpClientError(ref err) => Some(err),
Error::AuthError(ref err) => Some(err),
Error::JSONError(ref err) => Some(err),
Error::LowLevelError(ref err) => Some(err),
_ => None,
}
}
/// Produced by storage provider
#[error("Error while setting token in cache: {0}")]
StorageError(#[from] TokenStorageError),
/// Error while parsing credential source
#[error("Credential source is invalid: {0}")]
CredentialSourceError(CredentialSourceError),
}

#[cfg(test)]
Expand Down
41 changes: 37 additions & 4 deletions src/external_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use http_body_util::BodyExt;
use hyper_util::client::legacy::connect::Connect;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use url::form_urlencoded;

/// JSON schema of external account secret.
Expand Down Expand Up @@ -77,6 +78,23 @@ pub enum UrlCredentialSourceFormat {
},
}

#[derive(Debug, Error)]
/// Errors that can happen when parsing a Credential source
pub enum CredentialSourceError {
/// Parsing credential text source failed
#[error("Failed to parse credential text source: {0}")]
CredentialSourceTextInvalid(std::string::FromUtf8Error),
/// Failed to parse JSON
#[error("JSON credential source is invalid: {0}")]
JsonInvalid(#[source] serde_json::Error),
/// JSON is missing this field
#[error("JSON credential source is missing field {0}")]
MissingJsonField(String),
/// This field of JSON is invalid
#[error("JSON credential source could not convert field {0} to string")]
InvalidJsonField(String),
}

/// An ExternalAccountFlow can fetch OAuth tokens using an external account secret.
pub struct ExternalAccountFlow {
pub(crate) secret: ExternalAccountSecret,
Expand Down Expand Up @@ -116,15 +134,30 @@ impl ExternalAccountFlow {

match format {
UrlCredentialSourceFormat::Text => {
String::from_utf8(body.to_vec()).map_err(anyhow::Error::from)?
String::from_utf8(body.to_vec()).map_err(|e| {
Error::CredentialSourceError(
CredentialSourceError::CredentialSourceTextInvalid(e),
)
})?
}
UrlCredentialSourceFormat::Json {
subject_token_field_name,
} => serde_json::from_slice::<HashMap<String, serde_json::Value>>(&body)?
} => serde_json::from_slice::<HashMap<String, serde_json::Value>>(&body)
.map_err(|e| {
Error::CredentialSourceError(CredentialSourceError::JsonInvalid(e))
})?
.remove(subject_token_field_name)
.ok_or_else(|| anyhow::format_err!("missing {subject_token_field_name}"))?
.ok_or_else(|| {
Error::CredentialSourceError(CredentialSourceError::MissingJsonField(
subject_token_field_name.to_owned(),
))
})?
.as_str()
.ok_or_else(|| anyhow::format_err!("invalid type"))?
.ok_or_else(|| {
Error::CredentialSourceError(CredentialSourceError::InvalidJsonField(
subject_token_field_name.to_owned(),
))
})?
.to_string(),
}
}
Expand Down
16 changes: 14 additions & 2 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use futures::lock::Mutex;
use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use thiserror::Error;

use async_trait::async_trait;

Expand Down Expand Up @@ -106,13 +107,24 @@ where
}
}

#[derive(Debug, Error)]
/// Errors that occur while caching tokens in storage
pub enum TokenStorageError {
/// Error while performing an I/O action
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
/// Other errors
#[error("{0}")]
Other(std::borrow::Cow<'static, str>),
}

/// Implement your own token storage solution by implementing this trait. You need a way to
/// store and retrieve tokens, each keyed by a set of scopes.
#[async_trait]
pub trait TokenStorage: Send + Sync {
/// Store a token for the given set of scopes so that it can be retrieved later by get()
/// TokenInfo can be serialized with serde.
async fn set(&self, scopes: &[&str], token: TokenInfo) -> anyhow::Result<()>;
async fn set(&self, scopes: &[&str], token: TokenInfo) -> Result<(), TokenStorageError>;

/// Retrieve a token stored by set for the given set of scopes
async fn get(&self, scopes: &[&str]) -> Option<TokenInfo>;
Expand All @@ -129,7 +141,7 @@ impl Storage {
&self,
scopes: ScopeSet<'_, T>,
token: TokenInfo,
) -> anyhow::Result<()>
) -> Result<(), TokenStorageError>
where
T: AsRef<str>,
{
Expand Down

0 comments on commit d62c641

Please sign in to comment.