From e0daa91d2070d1905484b9044666daf7ea29a066 Mon Sep 17 00:00:00 2001 From: threema-donat <129288638+threema-donat@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:16:01 +0200 Subject: [PATCH] Use thiserror instead of anyhow --- Cargo.toml | 2 +- examples/custom_storage.rs | 18 +++--- src/error.rs | 113 ++++++++----------------------------- src/external_account.rs | 41 ++++++++++++-- src/storage.rs | 16 +++++- 5 files changed, 86 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0ad02b94..0998f0ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" diff --git a/examples/custom_storage.rs b/examples/custom_storage.rs index 6d44226e..e3be55d3 100644 --- a/examples/custom_storage.rs +++ b/examples/custom_storage.rs @@ -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>, @@ -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(), diff --git a/src/error.rs b/src/error.rs index b1445091..0144511b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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. /// @@ -142,104 +148,35 @@ impl AuthErrorOr { } /// 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 for Error { - fn from(error: hyper::Error) -> Error { - Error::HttpError(error) - } -} - -impl From for Error { - fn from(error: hyper_util::client::legacy::Error) -> Error { - Error::HttpClientError(error) - } -} - -impl From for Error { - fn from(value: AuthError) -> Error { - Error::AuthError(value) - } -} - -impl From for Error { - fn from(value: serde_json::Error) -> Error { - Error::JSONError(value) - } -} - -impl From for Error { - fn from(value: io::Error) -> Error { - Error::LowLevelError(value) - } -} - -impl From for Error { - fn from(value: anyhow::Error) -> Error { - match value.downcast::() { - 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)] diff --git a/src/external_account.rs b/src/external_account.rs index 1aebd166..03309855 100644 --- a/src/external_account.rs +++ b/src/external_account.rs @@ -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. @@ -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, @@ -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::>(&body)? + } => serde_json::from_slice::>(&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(), } } diff --git a/src/storage.rs b/src/storage.rs index c3fc0067..66b3eecd 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -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; @@ -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; @@ -129,7 +141,7 @@ impl Storage { &self, scopes: ScopeSet<'_, T>, token: TokenInfo, - ) -> anyhow::Result<()> + ) -> Result<(), TokenStorageError> where T: AsRef, {