Merge branch 'error-type'

This commit is contained in:
sunli 2020-04-02 21:05:44 +08:00
commit 1f41bb4a8e
4 changed files with 123 additions and 36 deletions

View File

@ -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))

View File

@ -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>,
},
}

View File

@ -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"
})),

View File

@ -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};