improve panic/unwrap message for user-typed errors (#110)

This commit is contained in:
Adam Leventhal 2022-07-08 11:24:37 -07:00 committed by GitHub
parent 03e2cfad3c
commit 236efadeee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 106 additions and 8 deletions

View File

@ -4,6 +4,10 @@ mod progenitor_client;
pub use crate::progenitor_client::*; pub use crate::progenitor_client::*;
// For stand-alone crates, rather than adding a dependency on
// progenitor-client, we simply dump the code right in. This means we don't
// need to determine the provenance of progenitor (crates.io, github, etc.)
// when generating the stand-alone crate.
#[doc(hidden)] #[doc(hidden)]
pub fn code() -> &'static str { pub fn code() -> &'static str {
include_str!("progenitor_client.rs") include_str!("progenitor_client.rs")

View File

@ -15,8 +15,23 @@ use reqwest::RequestBuilder;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
/// Represents an untyped byte stream for both success and error responses. /// Represents an untyped byte stream for both success and error responses.
pub type ByteStream = pub struct ByteStream(
Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>> + Send>>; Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>> + Send>>,
);
impl Deref for ByteStream {
type Target = Pin<Box<dyn Stream<Item = reqwest::Result<Bytes>> + Send>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ByteStream {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// Success value returned by generated client methods. /// Success value returned by generated client methods.
pub struct ResponseValue<T> { pub struct ResponseValue<T> {
@ -52,7 +67,7 @@ impl ResponseValue<ByteStream> {
let status = response.status(); let status = response.status();
let headers = response.headers().clone(); let headers = response.headers().clone();
Self { Self {
inner: Box::pin(response.bytes_stream()), inner: ByteStream(Box::pin(response.bytes_stream())),
status, status,
headers, headers,
} }
@ -75,6 +90,19 @@ impl ResponseValue<()> {
} }
impl<T> ResponseValue<T> { impl<T> ResponseValue<T> {
/// Create an instance for testing
pub fn new(
inner: T,
status: reqwest::StatusCode,
headers: reqwest::header::HeaderMap,
) -> Self {
Self {
inner,
status,
headers,
}
}
/// Consumes the ResponseValue, returning the wrapped value. /// Consumes the ResponseValue, returning the wrapped value.
pub fn into_inner(self) -> T { pub fn into_inner(self) -> T {
self.inner self.inner
@ -197,7 +225,10 @@ impl<E> From<reqwest::Error> for Error<E> {
} }
} }
impl<E> std::fmt::Display for Error<E> { impl<E> std::fmt::Display for Error<E>
where
ResponseValue<E>: ErrorFormat,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Error::InvalidRequest(s) => { Error::InvalidRequest(s) => {
@ -206,8 +237,9 @@ impl<E> std::fmt::Display for Error<E> {
Error::CommunicationError(e) => { Error::CommunicationError(e) => {
write!(f, "Communication Error: {}", e) write!(f, "Communication Error: {}", e)
} }
Error::ErrorResponse(_) => { Error::ErrorResponse(rve) => {
write!(f, "Error Response") write!(f, "Error Response: ")?;
rve.fmt_info(f)
} }
Error::InvalidResponsePayload(e) => { Error::InvalidResponsePayload(e) => {
write!(f, "Invalid Response Payload: {}", e) write!(f, "Invalid Response Payload: {}", e)
@ -218,12 +250,46 @@ impl<E> std::fmt::Display for Error<E> {
} }
} }
} }
impl<E> std::fmt::Debug for Error<E> {
trait ErrorFormat {
fn fmt_info(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
}
impl<E> ErrorFormat for ResponseValue<E>
where
E: std::fmt::Debug,
{
fn fmt_info(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"status: {}; headers: {:?}; value: {:?}",
self.status, self.headers, self.inner,
)
}
}
impl ErrorFormat for ResponseValue<ByteStream> {
fn fmt_info(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"status: {}; headers: {:?}; value: <stream>",
self.status, self.headers,
)
}
}
impl<E> std::fmt::Debug for Error<E>
where
ResponseValue<E>: ErrorFormat,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f) std::fmt::Display::fmt(self, f)
} }
} }
impl<E> std::error::Error for Error<E> { impl<E> std::error::Error for Error<E>
where
ResponseValue<E>: ErrorFormat,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { match self {
Error::CommunicationError(e) => Some(e), Error::CommunicationError(e) => Some(e),

View File

@ -0,0 +1,28 @@
// Copyright 2022 Oxide Computer Company
// Validate that we get useful output from a user-typed error.
#[test]
#[should_panic = "Error Response: \
status: 403 Forbidden; \
headers: {}; \
value: MyErr { msg: \"things went bad\" }"]
fn test_error() {
#[derive(Debug)]
struct MyErr {
#[allow(dead_code)]
msg: String,
}
let mine = MyErr {
msg: "things went bad".to_string(),
};
let e = progenitor_client::Error::ErrorResponse(
progenitor_client::ResponseValue::new(
mine,
reqwest::StatusCode::FORBIDDEN,
reqwest::header::HeaderMap::default(),
),
);
(Err(e) as Result<(), progenitor_client::Error<MyErr>>).unwrap();
}