Update error.rs

This commit is contained in:
sunli 2020-04-02 12:57:53 +08:00 committed by Nicolai Unrein
parent 506e82895f
commit 269f0f0d61
4 changed files with 126 additions and 39 deletions

View File

@ -4,6 +4,7 @@ extern crate thiserror;
use actix_rt; use actix_rt;
use actix_web::{guard, web, App, HttpResponse, HttpServer}; use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{graphiql_source, playground_source, GQLRequest, GQLResponse}; use async_graphql::http::{graphiql_source, playground_source, GQLRequest, GQLResponse};
use async_graphql::ErrorExtensions;
use async_graphql::*; use async_graphql::*;
use futures::TryFutureExt; use futures::TryFutureExt;
use serde_json::json; use serde_json::json;
@ -20,54 +21,87 @@ pub enum MyError {
ErrorWithoutExtensions, ErrorWithoutExtensions,
} }
impl MyError { impl ErrorExtensions for MyError {
fn extend_err(&self) -> serde_json::Value { // lets define our base extensions
match self { fn extend(&self) -> FieldError {
let extensions = match self {
MyError::NotFound => json!({"code": "NOT_FOUND"}), MyError::NotFound => json!({"code": "NOT_FOUND"}),
MyError::ServerError(reason) => json!({ "reason": reason }), MyError::ServerError(reason) => json!({ "reason": reason }),
MyError::ErrorWithoutExtensions => { MyError::ErrorWithoutExtensions => {
json!("This will be ignored since it does not represent an object.") json!("This will be ignored since it does not represent an object.")
} }
} };
}
}
fn get_my_error() -> std::result::Result<String, MyError> { FieldError(format!("{}", self), Some(extensions))
Err(MyError::ServerError("The database is locked".to_owned())) }
} }
struct QueryRoot; struct QueryRoot;
#[Object] #[Object]
impl QueryRoot { impl QueryRoot {
// It works on foreign types without extensions as before
#[field] #[field]
async fn do_not_find(&self) -> FieldResult<i32> { async fn parse_without_extensions(&self) -> FieldResult<i32> {
Err(MyError::NotFound).extend_err(MyError::extend_err) Ok("234a".parse()?)
} }
// Foreign types can be extended
#[field] #[field]
async fn fail(&self) -> FieldResult<String> { async fn parse_with_extensions(&self) -> FieldResult<i32> {
Ok(get_my_error().extend_err(MyError::extend_err)?) 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] #[field]
async fn without_extensions(&self) -> FieldResult<String> { async fn extend(&self) -> FieldResult<i32> {
Err(MyError::ErrorWithoutExtensions).extend_err(MyError::extend_err)? Err(MyError::NotFound.extend())?
} }
// Using the ResultExt trait, we can attach extensions on the fly capturing the execution // Or on the result
// environment. This method works on foreign types as well. The trait is implemented for all
// Results where the error variant implements `std::error::Error`.
#[field] #[field]
async fn parse_value(&self, val: String) -> FieldResult<i32> { async fn extend_result(&self) -> FieldResult<i32> {
val.parse().extend_err(|err| { Err(MyError::NotFound).extend()?
json!({ "description": format!("Could not parse value '{}': {}", val, err) })
})
} }
// Base extensions can be further extended
#[field] #[field]
async fn parse_value2(&self, val: String) -> FieldResult<i32> { async fn more_extensions(&self) -> FieldResult<String> {
Ok(val.parse()?) // 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] #[actix_rt::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
println!("Playground: http://localhost:8000");
println!("Graphiql: http://localhost:8000/graphiql");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.data(Schema::new(QueryRoot, EmptyMutation, EmptySubscription)) .data(Schema::new(QueryRoot, EmptyMutation, EmptySubscription))

View File

@ -1,10 +1,10 @@
use graphql_parser::query::{ParseError, Value}; use graphql_parser::query::{ParseError, Value};
use graphql_parser::Pos; use graphql_parser::Pos;
use std::error::Error as StdError;
use std::fmt::Debug; use std::fmt::Debug;
/// FieldError type /// 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 { impl FieldError {
#[doc(hidden)] #[doc(hidden)]
@ -37,10 +37,52 @@ pub type FieldResult<T> = std::result::Result<T, FieldError>;
impl<E> From<E> for FieldError impl<E> From<E> for FieldError
where where
E: StdError + Send + Sync + 'static, E: std::fmt::Display + Send + Sync + 'static,
{ {
fn from(err: E) -> Self { 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> pub trait ResultExt<T, E>
where where
Self: Sized, Self: Sized,
E: StdError + Send + Sync + 'static,
{ {
fn extend_err<CB>(self, cb: CB) -> FieldResult<T> fn extend_err<CB>(self, cb: CB) -> FieldResult<T>
where where
CB: FnOnce(&E) -> serde_json::Value; 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> impl<T, E> ResultExt<T, E> for std::result::Result<T, E>
where where
E: StdError + Send + Sync + 'static, E: ErrorExtensions + Send + Sync + 'static,
{ {
fn extend_err<C>(self, cb: C) -> FieldResult<T> fn extend_err<C>(self, cb: C) -> FieldResult<T>
where where
C: FnOnce(&E) -> serde_json::Value, C: FnOnce(&E) -> serde_json::Value,
{ {
match self { match self {
Err(err) => { Err(err) => Err(err.extend_with(|e| cb(e))),
let extended_err = cb(&err); Ok(value) => Ok(value),
Err(FieldError(err.into(), Some(extended_err)))
} }
}
fn extend(self) -> FieldResult<T> {
match self {
Err(err) => Err(err.extend()),
Ok(value) => Ok(value), Ok(value) => Ok(value),
} }
} }
@ -204,7 +253,7 @@ pub enum QueryError {
#[error("Failed to resolve field: {err}")] #[error("Failed to resolve field: {err}")]
FieldError { FieldError {
err: anyhow::Error, err: String,
extended_error: Option<serde_json::Value>, extended_error: Option<serde_json::Value>,
}, },
} }
@ -229,11 +278,11 @@ pub struct RuleError {
impl From<ParseError> for Error { impl From<ParseError> for Error {
fn from(err: ParseError) -> Self { fn from(err: ParseError) -> Self {
let msg = err.to_string(); let msg = err.to_string();
let mut s = msg.splitn(2, "\n"); let mut s = msg.splitn(2, '\n');
let first = s.next().unwrap(); let first = s.next().unwrap();
let ln = &first[first.rfind(" ").unwrap() + 1..]; let ln = &first[first.rfind(' ').unwrap() + 1..];
let (line, column) = { let (line, column) = {
let mut s = ln.splitn(2, ":"); let mut s = ln.splitn(2, ':');
( (
s.next().unwrap().parse().unwrap(), s.next().unwrap().parse().unwrap(),
s.next().unwrap().parse().unwrap(), s.next().unwrap().parse().unwrap(),

View File

@ -121,6 +121,7 @@ impl<'a> Serialize for GQLError<'a> {
if let Some(obj @ serde_json::Value::Object(_)) = extended_error { if let Some(obj @ serde_json::Value::Object(_)) = extended_error {
map.insert("extensions".to_string(), obj.clone()); map.insert("extensions".to_string(), obj.clone());
} }
seq.serialize_element(&serde_json::Value::Object(map))?; seq.serialize_element(&serde_json::Value::Object(map))?;
} else { } else {
seq.serialize_element(&serde_json::json!({ seq.serialize_element(&serde_json::json!({
@ -235,7 +236,7 @@ mod tests {
}, },
path: None, path: None,
err: QueryError::FieldError { err: QueryError::FieldError {
err: anyhow::anyhow!("MyErrorMessage"), err: "MyErrorMessage".to_owned(),
extended_error: Some(json!({ extended_error: Some(json!({
"code": "MY_TEST_CODE" "code": "MY_TEST_CODE"
})), })),

View File

@ -100,7 +100,7 @@ pub mod http;
pub use base::{Scalar, Type}; pub use base::{Scalar, Type};
pub use context::{Context, QueryPathSegment, Variables}; 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::query::Value;
pub use graphql_parser::Pos; pub use graphql_parser::Pos;
pub use query::{QueryBuilder, QueryResponse}; pub use query::{QueryBuilder, QueryResponse};