diff --git a/.github/workflows/rust-workflow.yml b/.github/workflows/rust-workflow.yml index cb09aad..fa0eb8f 100644 --- a/.github/workflows/rust-workflow.yml +++ b/.github/workflows/rust-workflow.yml @@ -20,5 +20,9 @@ env: # Defined CI jobs. jobs: check: - uses: famedly/backend-build-workflows/.github/workflows/rust-workflow.yml@v1 + uses: famedly/backend-build-workflows/.github/workflows/rust-workflow.yml@main secrets: inherit + with: + clippy_args: '--all-features' + test_args: '--all-features' + testcov_args: '--all-features' diff --git a/Cargo.toml b/Cargo.toml index ea9f105..1a795bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ resolver = "2" publish = ["famedly"] [dependencies] +reqwest = { version = "0.12.12", optional = true } serde = { version = "1.0.210", features = ["derive"] } thiserror = "1.0.64" time = { version = "0.3.36", optional = true } @@ -17,6 +18,7 @@ url = { version = "2.5.2", features = ["serde"] } serde_json = "1.0.128" [features] +reqwest = ["dep:reqwest"] time = ["dep:time"] [lints.rust] diff --git a/src/lib.rs b/src/lib.rs index 84a45f9..dd5f5ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,9 @@ mod base_url; pub mod duration; /// [serde::Deserialize] impl for [tracing::level_filters::LevelFilter] mod level_filter; +#[cfg(feature = "reqwest")] +/// Helpers for [reqwest] +pub mod reqwest; pub use base_url::{BaseUrl, BaseUrlParseError}; pub use level_filter::LevelFilter; diff --git a/src/reqwest.rs b/src/reqwest.rs new file mode 100644 index 0000000..50e7517 --- /dev/null +++ b/src/reqwest.rs @@ -0,0 +1,58 @@ +use std::{fmt, future::Future}; + +/// Wrapper around [reqwest::Error] with optional response body +#[derive(Debug, thiserror::Error)] +pub struct ReqwestErrorWithBody { + /// Error from [reqwest] + pub error: reqwest::Error, + /// Optional response body + pub body: Option, +} + +impl From for ReqwestErrorWithBody { + fn from(error: reqwest::Error) -> ReqwestErrorWithBody { + Self { error, body: None } + } +} + +impl fmt::Display for ReqwestErrorWithBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} with body {}", self.error, self.body.as_deref().unwrap_or("")) + } +} + +/// An alternative to [reqwest::Resnpose::error_for_status] that also returns +/// optional response body +/// ```no_run +/// # use famedly_rust_utils::reqwest::*; +/// # async fn mk_req() -> Result<(), ReqwestErrorWithBody> { +/// reqwest::get("http://invalid.example") +/// .await? +/// .error_for_status_with_body() +/// .await?; +/// # Ok(()) +/// # } +/// ``` +pub trait ErrorForStatusWithBody { + // Using explicit `impl Future` syntax here instead of `async_fn_in_trait` to + // make it `Send` + #[allow(missing_docs)] + fn error_for_status_with_body( + self, + ) -> impl Future> + Send; +} + +impl ErrorForStatusWithBody for reqwest::Response { + #[allow(clippy::manual_async_fn)] + fn error_for_status_with_body( + self, + ) -> impl Future> + Send { + async { + if let Err(error) = self.error_for_status_ref() { + Err(ReqwestErrorWithBody { error, body: self.text().await.ok() }) + } else { + Ok(self) + } + } + } +}