From 269f0f0d61da43426a2180790cebed9c715a5913 Mon Sep 17 00:00:00 2001 From: sunli Date: Thu, 2 Apr 2020 12:57:53 +0800 Subject: [PATCH] Update error.rs --- examples/error_extensions.rs | 83 ++++++++++++++++++++++++++---------- src/error.rs | 77 +++++++++++++++++++++++++++------ src/http/mod.rs | 3 +- src/lib.rs | 2 +- 4 files changed, 126 insertions(+), 39 deletions(-) diff --git a/examples/error_extensions.rs b/examples/error_extensions.rs index 87b686a6..3e483d16 100644 --- a/examples/error_extensions.rs +++ b/examples/error_extensions.rs @@ -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 { - 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 { - Err(MyError::NotFound).extend_err(MyError::extend_err) + async fn parse_without_extensions(&self) -> FieldResult { + Ok("234a".parse()?) } + // Foreign types can be extended #[field] - async fn fail(&self) -> FieldResult { - Ok(get_my_error().extend_err(MyError::extend_err)?) + async fn parse_with_extensions(&self) -> FieldResult { + 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 { + // 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 { - Err(MyError::ErrorWithoutExtensions).extend_err(MyError::extend_err)? + async fn extend(&self) -> FieldResult { + 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 { - val.parse().extend_err(|err| { - json!({ "description": format!("Could not parse value '{}': {}", val, err) }) - }) + async fn extend_result(&self) -> FieldResult { + Err(MyError::NotFound).extend()? } + // Base extensions can be further extended #[field] - async fn parse_value2(&self, val: String) -> FieldResult { - Ok(val.parse()?) + async fn more_extensions(&self) -> FieldResult { + // 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 { + // 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 { + 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 { + 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)) diff --git a/src/error.rs b/src/error.rs index 5b83d3ca..910160ac 100644 --- a/src/error.rs +++ b/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); +#[derive(Clone, Debug)] +pub struct FieldError(pub String, pub Option); impl FieldError { #[doc(hidden)] @@ -37,10 +37,52 @@ pub type FieldResult = std::result::Result; impl From 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(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 ErrorExtensions for &E { + fn extend(&self) -> FieldError { + FieldError(format!("{}", self), None) } } @@ -48,26 +90,33 @@ where pub trait ResultExt where Self: Sized, - E: StdError + Send + Sync + 'static, { fn extend_err(self, cb: CB) -> FieldResult where CB: FnOnce(&E) -> serde_json::Value; + + fn extend(self) -> FieldResult; } +// This is implemented on E and not &E which means it cannot be used on foreign types. +// (see example). impl ResultExt for std::result::Result where - E: StdError + Send + Sync + 'static, + E: ErrorExtensions + Send + Sync + 'static, { fn extend_err(self, cb: C) -> FieldResult 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 { + 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, }, } @@ -229,11 +278,11 @@ pub struct RuleError { impl From for Error { fn from(err: ParseError) -> Self { 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 ln = &first[first.rfind(" ").unwrap() + 1..]; + let ln = &first[first.rfind(' ').unwrap() + 1..]; 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(), diff --git a/src/http/mod.rs b/src/http/mod.rs index 1106b825..f6eca550 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -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" })), diff --git a/src/lib.rs b/src/lib.rs index cb900062..403143d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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};