diff --git a/CHANGELOG.md b/CHANGELOG.md index 5784c6c3..d7908632 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `chrono::Duration` custom scalar. [#689](https://github.com/async-graphql/async-graphql/pull/689) - Implement `From>>` for `MaybeUndefined`. - Add `MaybeUndefined::as_opt_ref` and `MaybeUndefined::as_opt_deref` methods. +- Add `Failure` type. [#671](https://github.com/async-graphql/async-graphql/issues/671) ## [2.11.0] 2021-11-03 diff --git a/src/error.rs b/src/error.rs index e0c0485e..5bdb3401 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,6 +24,11 @@ impl ErrorExtensionValues { pub struct ServerError { /// An explanatory message of the error. pub message: String, + /// An explanatory message of the error. (for debug) + /// + /// This message comes from [`Failure`]. + #[serde(skip)] + pub debug_message: Option, /// Where the error occurred. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub locations: Vec, @@ -44,12 +49,18 @@ impl ServerError { pub fn new(message: impl Into, pos: Option) -> Self { Self { message: message.into(), + debug_message: None, locations: pos.map(|pos| vec![pos]).unwrap_or_default(), path: Vec::new(), extensions: None, } } + /// Returns `debug_message` if it is `Some`, otherwise returns `message`. + pub fn debug_message(&self) -> &str { + self.debug_message.as_deref().unwrap_or(&self.message) + } + #[doc(hidden)] pub fn with_path(self, path: Vec) -> Self { Self { path, ..self } @@ -72,6 +83,7 @@ impl From for ServerError { fn from(e: parser::Error) -> Self { Self { message: e.to_string(), + debug_message: None, locations: e.positions().collect(), path: Vec::new(), extensions: None, @@ -164,6 +176,8 @@ pub type InputValueResult = Result>; pub struct Error { /// The error message. pub message: String, + /// The debug error message. + pub debug_message: Option, /// Extensions to the error. #[serde(skip_serializing_if = "error_extensions_is_empty")] pub extensions: Option, @@ -174,6 +188,7 @@ impl Error { pub fn new(message: impl Into) -> Self { Self { message: message.into(), + debug_message: None, extensions: None, } } @@ -183,6 +198,7 @@ impl Error { pub fn into_server_error(self, pos: Pos) -> ServerError { ServerError { message: self.message, + debug_message: self.debug_message, locations: vec![pos], path: Vec::new(), extensions: self.extensions, @@ -194,6 +210,17 @@ impl From for Error { fn from(e: T) -> Self { Self { message: e.to_string(), + debug_message: None, + extensions: None, + } + } +} + +impl From> for Error { + fn from(err: Failure) -> Self { + Self { + message: format!("{}", err.0), + debug_message: Some(format!("{:?}", err.0)), extensions: None, } } @@ -267,40 +294,57 @@ impl From for ParseRequestError { /// An error which can be extended into a `Error`. pub trait ErrorExtensions: Sized { /// Convert the error to a `Error`. - fn extend(&self) -> Error; + fn extend(self) -> Error; /// Add extensions to the error, using a callback to make the extensions. fn extend_with(self, cb: C) -> Error where C: FnOnce(&Self, &mut ErrorExtensionValues), { - let message = self.extend().message; - let mut extensions = self.extend().extensions.unwrap_or_default(); - cb(&self, &mut extensions); + let mut new_extensions = Default::default(); + cb(&self, &mut new_extensions); + + let Error { + message, + debug_message, + extensions, + } = self.extend(); + + let mut extensions = extensions.unwrap_or_default(); + extensions.0.extend(new_extensions.0); + Error { message, + debug_message, extensions: Some(extensions), } } } impl ErrorExtensions for Error { - fn extend(&self) -> Error { - self.clone() + fn extend(self) -> Error { + self } } // implementing for &E instead of E gives the user the possibility to implement for E which does // not conflict with this implementation acting as a fallback. impl ErrorExtensions for &E { - fn extend(&self) -> Error { + fn extend(self) -> Error { Error { message: self.to_string(), + debug_message: None, extensions: None, } } } +impl ErrorExtensions for Failure { + fn extend(self) -> Error { + Error::from(self) + } +} + /// Extend a `Result`'s error value with [`ErrorExtensions`](trait.ErrorExtensions.html). pub trait ResultExt: Sized { /// Extend the error value of the result with the callback. @@ -335,3 +379,23 @@ where } } } + +/// An error type contains `T` that implements `Display` and `Debug`. +/// +/// This type can solve the problem described in [#671](https://github.com/async-graphql/async-graphql/issues/671). +#[derive(Debug)] +pub struct Failure(pub T); + +impl From for Failure { + fn from(err: T) -> Self { + Self(err) + } +} + +impl Failure { + /// Create a new failure. + #[inline] + pub fn new(err: T) -> Self { + Self(err) + } +} diff --git a/src/lib.rs b/src/lib.rs index 1aca1636..a65539fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -209,7 +209,7 @@ pub use base::{ Type, UnionType, }; pub use error::{ - Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult, + Error, ErrorExtensionValues, ErrorExtensions, Failure, InputValueError, InputValueResult, ParseRequestError, PathSegment, Result, ResultExt, ServerError, ServerResult, }; pub use look_ahead::Lookahead; diff --git a/src/validation/visitor.rs b/src/validation/visitor.rs index 0a6024c7..e053fa41 100644 --- a/src/validation/visitor.rs +++ b/src/validation/visitor.rs @@ -851,6 +851,7 @@ impl From for ServerError { fn from(e: RuleError) -> Self { Self { message: e.message, + debug_message: None, locations: e.locations, path: Vec::new(), extensions: e.extensions, diff --git a/tests/error_ext.rs b/tests/error_ext.rs index 6214b08d..34a0c991 100644 --- a/tests/error_ext.rs +++ b/tests/error_ext.rs @@ -1,4 +1,5 @@ use async_graphql::*; +use std::fmt::{self, Display, Formatter}; #[tokio::test] pub async fn test_error_extensions() { @@ -75,3 +76,114 @@ pub async fn test_error_extensions() { }) ); } + +#[tokio::test] +pub async fn test_failure() { + #[derive(Debug)] + struct MyError; + + impl Display for MyError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "my error") + } + } + + struct Query; + + #[Object] + impl Query { + async fn failure(&self) -> Result { + Err(Failure(MyError).into()) + } + + async fn failure2(&self) -> Result { + Err(Failure(MyError))?; + Ok(1) + } + + async fn failure3(&self) -> Result { + Err(Failure(MyError) + .extend_with(|_, values| values.set("a", 1)) + .extend_with(|_, values| values.set("b", 2)))?; + Ok(1) + } + + async fn failure4(&self) -> Result { + Err(Failure(MyError)) + .extend_err(|_, values| values.set("a", 1)) + .extend_err(|_, values| values.set("b", 2))?; + Ok(1) + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema + .execute("{ failure }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "my error".to_string(), + debug_message: Some("MyError".to_string()), + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("failure".to_string())], + extensions: None + }] + ); + + assert_eq!( + schema + .execute("{ failure2 }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "my error".to_string(), + debug_message: Some("MyError".to_string()), + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("failure2".to_string())], + extensions: None + }] + ); + + assert_eq!( + schema + .execute("{ failure3 }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "my error".to_string(), + debug_message: Some("MyError".to_string()), + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("failure3".to_string())], + extensions: Some({ + let mut values = ErrorExtensionValues::default(); + values.set("a", 1); + values.set("b", 2); + values + }) + }] + ); + + assert_eq!( + schema + .execute("{ failure4 }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "my error".to_string(), + debug_message: Some("MyError".to_string()), + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("failure4".to_string())], + extensions: Some({ + let mut values = ErrorExtensionValues::default(); + values.set("a", 1); + values.set("b", 2); + values + }) + }] + ); +}