Merge branch 'error-type'
This commit is contained in:
commit
1f41bb4a8e
|
@ -4,6 +4,7 @@ extern crate thiserror;
|
|||
use actix_rt;
|
||||
use actix_web::{guard, web, App, HttpResponse, HttpServer};
|
||||
use async_graphql::http::{graphiql_source, playground_source, GQLRequest, GQLResponse};
|
||||
use async_graphql::ErrorExtensions;
|
||||
use async_graphql::*;
|
||||
use futures::TryFutureExt;
|
||||
use serde_json::json;
|
||||
|
@ -20,54 +21,87 @@ pub enum MyError {
|
|||
ErrorWithoutExtensions,
|
||||
}
|
||||
|
||||
impl MyError {
|
||||
fn extend_err(&self) -> serde_json::Value {
|
||||
match self {
|
||||
impl ErrorExtensions for MyError {
|
||||
// lets define our base extensions
|
||||
fn extend(&self) -> FieldError {
|
||||
let extensions = match self {
|
||||
MyError::NotFound => json!({"code": "NOT_FOUND"}),
|
||||
MyError::ServerError(reason) => json!({ "reason": reason }),
|
||||
MyError::ErrorWithoutExtensions => {
|
||||
json!("This will be ignored since it does not represent an object.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn get_my_error() -> std::result::Result<String, MyError> {
|
||||
Err(MyError::ServerError("The database is locked".to_owned()))
|
||||
FieldError(format!("{}", self), Some(extensions))
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryRoot;
|
||||
|
||||
#[Object]
|
||||
impl QueryRoot {
|
||||
// It works on foreign types without extensions as before
|
||||
#[field]
|
||||
async fn do_not_find(&self) -> FieldResult<i32> {
|
||||
Err(MyError::NotFound).extend_err(MyError::extend_err)
|
||||
async fn parse_without_extensions(&self) -> FieldResult<i32> {
|
||||
Ok("234a".parse()?)
|
||||
}
|
||||
|
||||
// Foreign types can be extended
|
||||
#[field]
|
||||
async fn fail(&self) -> FieldResult<String> {
|
||||
Ok(get_my_error().extend_err(MyError::extend_err)?)
|
||||
async fn parse_with_extensions(&self) -> FieldResult<i32> {
|
||||
Ok("234a"
|
||||
.parse()
|
||||
.map_err(|e: std::num::ParseIntError| e.extend_with(|_| json!({"code": 404})))?)
|
||||
}
|
||||
|
||||
// THIS does unfortunately NOT work because ErrorExtensions is implemented for &E and not E.
|
||||
// Which is necessary for the overwrite by the user.
|
||||
|
||||
//#[field]
|
||||
// async fn parse_with_extensions_result(&self) -> FieldResult<i32> {
|
||||
// Ok("234a".parse().extend_err(|_| json!({"code": 404}))?)
|
||||
// }
|
||||
|
||||
// Using our own types we can implement some base extensions
|
||||
#[field]
|
||||
async fn without_extensions(&self) -> FieldResult<String> {
|
||||
Err(MyError::ErrorWithoutExtensions).extend_err(MyError::extend_err)?
|
||||
async fn extend(&self) -> FieldResult<i32> {
|
||||
Err(MyError::NotFound.extend())?
|
||||
}
|
||||
|
||||
// Using the ResultExt trait, we can attach extensions on the fly capturing the execution
|
||||
// environment. This method works on foreign types as well. The trait is implemented for all
|
||||
// Results where the error variant implements `std::error::Error`.
|
||||
// Or on the result
|
||||
#[field]
|
||||
async fn parse_value(&self, val: String) -> FieldResult<i32> {
|
||||
val.parse().extend_err(|err| {
|
||||
json!({ "description": format!("Could not parse value '{}': {}", val, err) })
|
||||
})
|
||||
async fn extend_result(&self) -> FieldResult<i32> {
|
||||
Err(MyError::NotFound).extend()?
|
||||
}
|
||||
|
||||
// Base extensions can be further extended
|
||||
#[field]
|
||||
async fn parse_value2(&self, val: String) -> FieldResult<i32> {
|
||||
Ok(val.parse()?)
|
||||
async fn more_extensions(&self) -> FieldResult<String> {
|
||||
// resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" }
|
||||
Err(MyError::NotFound.extend_with(|_e| json!({"reason": "my reason"})))?
|
||||
}
|
||||
|
||||
// works with results as well
|
||||
#[field]
|
||||
async fn more_extensions_on_result(&self) -> FieldResult<String> {
|
||||
// resolves to extensions: { "code": "NOT_FOUND", "reason": "my reason" }
|
||||
Err(MyError::NotFound).extend_err(|_e| json!({"reason": "my reason"}))?
|
||||
}
|
||||
|
||||
// extend_with is chainable
|
||||
#[field]
|
||||
async fn chainable_extensions(&self) -> FieldResult<String> {
|
||||
let err = MyError::NotFound
|
||||
.extend_with(|_| json!({"ext1": 1}))
|
||||
.extend_with(|_| json!({"ext2": 2}))
|
||||
.extend_with(|_| json!({"ext3": 3}));
|
||||
Err(err)?
|
||||
}
|
||||
|
||||
// extend_with overwrites keys which are already present
|
||||
#[field]
|
||||
async fn overwrite(&self) -> FieldResult<String> {
|
||||
Err(MyError::NotFound.extend_with(|_| json!({"code": "overwritten"})))?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,6 +130,9 @@ async fn gql_graphiql() -> HttpResponse {
|
|||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
println!("Playground: http://localhost:8000");
|
||||
println!("Graphiql: http://localhost:8000/graphiql");
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.data(Schema::new(QueryRoot, EmptyMutation, EmptySubscription))
|
||||
|
|
71
src/error.rs
71
src/error.rs
|
@ -1,10 +1,10 @@
|
|||
use graphql_parser::query::{ParseError, Value};
|
||||
use graphql_parser::Pos;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// FieldError type
|
||||
pub struct FieldError(anyhow::Error, Option<serde_json::Value>);
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FieldError(pub String, pub Option<serde_json::Value>);
|
||||
|
||||
impl FieldError {
|
||||
#[doc(hidden)]
|
||||
|
@ -37,10 +37,52 @@ pub type FieldResult<T> = std::result::Result<T, FieldError>;
|
|||
|
||||
impl<E> From<E> for FieldError
|
||||
where
|
||||
E: StdError + Send + Sync + 'static,
|
||||
E: std::fmt::Display + Send + Sync + 'static,
|
||||
{
|
||||
fn from(err: E) -> Self {
|
||||
FieldError(anyhow::Error::from(err), None)
|
||||
FieldError(format!("{}", err), None)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
pub trait ErrorExtensions
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn extend(&self) -> FieldError;
|
||||
fn extend_with<C>(self, cb: C) -> FieldError
|
||||
where
|
||||
C: FnOnce(&Self) -> serde_json::Value,
|
||||
{
|
||||
let name = self.extend().0;
|
||||
|
||||
if let Some(mut base) = self.extend().1 {
|
||||
let mut cb_res = cb(&self);
|
||||
if let Some(base_map) = base.as_object_mut() {
|
||||
if let Some(cb_res_map) = cb_res.as_object_mut() {
|
||||
base_map.append(cb_res_map);
|
||||
}
|
||||
return FieldError(name, Some(serde_json::json!(base_map)));
|
||||
} else {
|
||||
return FieldError(name, Some(cb_res));
|
||||
}
|
||||
}
|
||||
|
||||
FieldError(name, Some(cb(&self)))
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorExtensions for FieldError {
|
||||
fn extend(&self) -> FieldError {
|
||||
self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// 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<E: std::fmt::Display> ErrorExtensions for &E {
|
||||
fn extend(&self) -> FieldError {
|
||||
FieldError(format!("{}", self), None)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,26 +90,33 @@ where
|
|||
pub trait ResultExt<T, E>
|
||||
where
|
||||
Self: Sized,
|
||||
E: StdError + Send + Sync + 'static,
|
||||
{
|
||||
fn extend_err<CB>(self, cb: CB) -> FieldResult<T>
|
||||
where
|
||||
CB: FnOnce(&E) -> serde_json::Value;
|
||||
|
||||
fn extend(self) -> FieldResult<T>;
|
||||
}
|
||||
|
||||
// This is implemented on E and not &E which means it cannot be used on foreign types.
|
||||
// (see example).
|
||||
impl<T, E> ResultExt<T, E> for std::result::Result<T, E>
|
||||
where
|
||||
E: StdError + Send + Sync + 'static,
|
||||
E: ErrorExtensions + Send + Sync + 'static,
|
||||
{
|
||||
fn extend_err<C>(self, cb: C) -> FieldResult<T>
|
||||
where
|
||||
C: FnOnce(&E) -> serde_json::Value,
|
||||
{
|
||||
match self {
|
||||
Err(err) => {
|
||||
let extended_err = cb(&err);
|
||||
Err(FieldError(err.into(), Some(extended_err)))
|
||||
}
|
||||
Err(err) => Err(err.extend_with(|e| cb(e))),
|
||||
Ok(value) => Ok(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn extend(self) -> FieldResult<T> {
|
||||
match self {
|
||||
Err(err) => Err(err.extend()),
|
||||
Ok(value) => Ok(value),
|
||||
}
|
||||
}
|
||||
|
@ -204,7 +253,7 @@ pub enum QueryError {
|
|||
|
||||
#[error("Failed to resolve field: {err}")]
|
||||
FieldError {
|
||||
err: anyhow::Error,
|
||||
err: String,
|
||||
extended_error: Option<serde_json::Value>,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ impl<'a> Serialize for GQLError<'a> {
|
|||
if let Some(obj @ serde_json::Value::Object(_)) = extended_error {
|
||||
map.insert("extensions".to_string(), obj.clone());
|
||||
}
|
||||
|
||||
seq.serialize_element(&serde_json::Value::Object(map))?;
|
||||
} else {
|
||||
seq.serialize_element(&serde_json::json!({
|
||||
|
@ -235,7 +236,7 @@ mod tests {
|
|||
},
|
||||
path: None,
|
||||
err: QueryError::FieldError {
|
||||
err: anyhow::anyhow!("MyErrorMessage"),
|
||||
err: "MyErrorMessage".to_owned(),
|
||||
extended_error: Some(json!({
|
||||
"code": "MY_TEST_CODE"
|
||||
})),
|
||||
|
|
|
@ -100,7 +100,7 @@ pub mod http;
|
|||
|
||||
pub use base::{Scalar, Type};
|
||||
pub use context::{Context, QueryPathSegment, Variables};
|
||||
pub use error::{Error, FieldError, FieldResult, QueryError, ResultExt};
|
||||
pub use error::{Error, ErrorExtensions, FieldError, FieldResult, QueryError, ResultExt};
|
||||
pub use graphql_parser::query::Value;
|
||||
pub use graphql_parser::Pos;
|
||||
pub use query::{QueryBuilder, QueryResponse};
|
||||
|
|
Loading…
Reference in New Issue
Block a user