From 14860d9b885bdf3f19c0244572117f609f910703 Mon Sep 17 00:00:00 2001 From: sunli Date: Thu, 2 Apr 2020 10:21:04 +0800 Subject: [PATCH] Reimplement the error type and remove the dependency on the anyhow::Error --- .../examples/subscription.rs | 4 +- async-graphql-derive/src/enum.rs | 2 +- async-graphql-derive/src/interface.rs | 19 +- async-graphql-derive/src/object.rs | 25 +- async-graphql-derive/src/output_type.rs | 4 +- async-graphql-derive/src/simple_object.rs | 12 +- async-graphql-derive/src/subscription.rs | 10 +- async-graphql-derive/src/union.rs | 13 +- examples/error_extensions.rs | 64 +++--- examples/starwars/model.rs | 6 +- src/base.rs | 44 ++-- src/context.rs | 53 +++-- src/error.rs | 214 +++++++++--------- src/extensions/tracing.rs | 12 +- src/http/mod.rs | 186 ++++++--------- src/lib.rs | 16 +- src/query.rs | 24 +- src/resolver.rs | 38 ++-- src/scalars/string.rs | 10 +- src/schema.rs | 15 +- src/subscription/connection.rs | 5 +- src/subscription/ws_transport.rs | 14 +- src/types/connection/connection_type.rs | 35 +-- src/types/connection/edge.rs | 14 +- src/types/connection/mod.rs | 52 +---- src/types/connection/slice.rs | 4 +- src/types/empty_mutation.rs | 16 +- src/types/empty_subscription.rs | 15 +- src/types/list.rs | 17 +- src/types/optional.rs | 13 +- src/types/query_root.rs | 34 +-- src/validation/mod.rs | 5 +- 32 files changed, 491 insertions(+), 504 deletions(-) diff --git a/async-graphql-actix-web/examples/subscription.rs b/async-graphql-actix-web/examples/subscription.rs index ce52449d..b096885f 100644 --- a/async-graphql-actix-web/examples/subscription.rs +++ b/async-graphql-actix-web/examples/subscription.rs @@ -1,5 +1,5 @@ use actix_web::{web, App, HttpServer}; -use async_graphql::{publish, Context, Result, Schema, ID}; +use async_graphql::{publish, Context, FieldResult, Schema, ID}; use futures::lock::Mutex; use slab::Slab; use std::sync::Arc; @@ -66,7 +66,7 @@ impl MutationRoot { } #[field] - async fn delete_book(&self, ctx: &Context<'_>, id: ID) -> Result { + async fn delete_book(&self, ctx: &Context<'_>, id: ID) -> FieldResult { let mut books = ctx.data::().lock().await; let id = id.parse::()?; if books.contains(id) { diff --git a/async-graphql-derive/src/enum.rs b/async-graphql-derive/src/enum.rs index 1afd2292..4536cb02 100644 --- a/async-graphql-derive/src/enum.rs +++ b/async-graphql-derive/src/enum.rs @@ -112,7 +112,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(value: &Self, _: &#crate_name::ContextSelectionSet<'_>, _pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::EnumType::resolve_enum(value) } } diff --git a/async-graphql-derive/src/interface.rs b/async-graphql-derive/src/interface.rs index 0f0c7657..fe38045f 100644 --- a/async-graphql-derive/src/interface.rs +++ b/async-graphql-derive/src/interface.rs @@ -117,7 +117,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result None => quote! { || #crate_name::Value::Null }, }; get_params.push(quote! { - let #ident: #ty = ctx.param_value(#name, #param_default)?; + let #ident: #ty = ctx.param_value(#name, field.position, #param_default)?; }); let desc = desc @@ -205,8 +205,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result if field.name.as_str() == #name { #(#get_params)* let ctx_obj = ctx.with_selection_set(&field.selection_set); - return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj).await. - map_err(|err| err.with_position(field.position).into()); + return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj, field.position).await; } }); } @@ -251,34 +250,32 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #[#crate_name::async_trait::async_trait] impl #generics #crate_name::ObjectType for #ident #generics { async fn resolve_field(&self, ctx: &#crate_name::Context<'_>, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { - use #crate_name::ErrorWithPosition; - #(#resolvers)* - #crate_name::anyhow::bail!(#crate_name::QueryError::FieldNotFound { + Err(#crate_name::QueryError::FieldNotFound { field_name: field.name.clone(), object: #gql_typename.to_string(), - } - .with_position(field.position)); + }.into_error(field.position)) } fn collect_inline_fields<'a>( &'a self, name: &str, + pos: #crate_name::Pos, ctx: &#crate_name::ContextSelectionSet<'a>, futures: &mut Vec<#crate_name::BoxFieldFuture<'a>>, ) -> #crate_name::Result<()> { #(#collect_inline_fields)* - #crate_name::anyhow::bail!(#crate_name::QueryError::UnrecognizedInlineFragment { + Err(#crate_name::QueryError::UnrecognizedInlineFragment { object: #gql_typename.to_string(), name: name.to_string(), - }); + }.into_error(pos)) } } #[#crate_name::async_trait::async_trait] impl #generics #crate_name::OutputValueType for #ident #generics { - async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>, pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, value).await } } diff --git a/async-graphql-derive/src/object.rs b/async-graphql-derive/src/object.rs index 049022c2..77113377 100644 --- a/async-graphql-derive/src/object.rs +++ b/async-graphql-derive/src/object.rs @@ -174,7 +174,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< }; get_params.push(quote! { - let #ident: #ty = ctx.param_value(#name, #default)?; + let #ident: #ty = ctx.param_value(#name, field.position, #default)?; }); } @@ -195,7 +195,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< }); }); - let ctx_field = if arg_ctx { + let ctx_param = if arg_ctx { quote! { &ctx, } } else { quote! {} @@ -204,12 +204,14 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< let field_ident = &method.sig.ident; let resolve_obj = match &ty { OutputType::Value(_) => quote! { - self.#field_ident(#ctx_field #(#use_params),*).await + self.#field_ident(#ctx_param #(#use_params),*).await }, OutputType::Result(_, _) => { quote! { - self.#field_ident(#ctx_field #(#use_params),*).await. - map_err(|err| err.with_position(field.position))? + { + let res:#crate_name::FieldResult<_> = self.#field_ident(#ctx_param #(#use_params),*).await; + res.map_err(|err| err.into_error_with_path(field.position, ctx.path_node.as_ref().unwrap().to_json()))? + } } } }; @@ -218,8 +220,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< if field.name.as_str() == #field_name { #(#get_params)* let ctx_obj = ctx.with_selection_set(&field.selection_set); - return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj).await. - map_err(|err| err.with_position(field.position).into()); + return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj, field.position).await; } }); @@ -272,21 +273,17 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< #[#crate_name::async_trait::async_trait] impl#generics #crate_name::ObjectType for #self_ty { async fn resolve_field(&self, ctx: &#crate_name::Context<'_>, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { - use #crate_name::ErrorWithPosition; - #(#resolvers)* - - #crate_name::anyhow::bail!(#crate_name::QueryError::FieldNotFound { + Err(#crate_name::QueryError::FieldNotFound { field_name: field.name.clone(), object: #gql_typename.to_string(), - } - .with_position(field.position)); + }.into_error(field.position)) } } #[#crate_name::async_trait::async_trait] impl #generics #crate_name::OutputValueType for #self_ty { - async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>, pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, value).await } } diff --git a/async-graphql-derive/src/output_type.rs b/async-graphql-derive/src/output_type.rs index 1112a3b7..8fa87025 100644 --- a/async-graphql-derive/src/output_type.rs +++ b/async-graphql-derive/src/output_type.rs @@ -10,7 +10,9 @@ pub enum OutputType<'a> { impl<'a> OutputType<'a> { pub fn parse(input: &'a Type) -> Result { let ty = if let Type::Path(p) = input { - if p.path.segments.last().unwrap().ident == "Result" { + if p.path.segments.last().unwrap().ident == "Result" + || p.path.segments.last().unwrap().ident == "FieldResult" + { if let PathArguments::AngleBracketed(args) = &p.path.segments[0].arguments { if args.args.is_empty() { return Err(Error::new_spanned(input, "Invalid type")); diff --git a/async-graphql-derive/src/simple_object.rs b/async-graphql-derive/src/simple_object.rs index 4c3fb8f5..99a546d7 100644 --- a/async-graphql-derive/src/simple_object.rs +++ b/async-graphql-derive/src/simple_object.rs @@ -76,8 +76,7 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result Result, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { - use #crate_name::ErrorWithPosition; - #(#resolvers)* - #crate_name::anyhow::bail!(#crate_name::QueryError::FieldNotFound { + Err(#crate_name::QueryError::FieldNotFound { field_name: field.name.clone(), object: #gql_typename.to_string(), - } - .with_position(field.position)); + }.into_error(field.position)) } } #[#crate_name::async_trait::async_trait] impl #generics #crate_name::OutputValueType for #ident #generics { - async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>, _pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, value).await } } diff --git a/async-graphql-derive/src/subscription.rs b/async-graphql-derive/src/subscription.rs index 06264eb9..0f6dca38 100644 --- a/async-graphql-derive/src/subscription.rs +++ b/async-graphql-derive/src/subscription.rs @@ -181,7 +181,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< }; get_params.push(quote! { - let #ident: #ty = ctx_field.param_value(#name, #default)?; + let #ident: #ty = ctx_field.param_value(#name, field.position, #default)?; }); } @@ -213,7 +213,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< if self.#ident(msg, #(#use_params)*) { let ctx_selection_set = ctx_field.with_selection_set(&field.selection_set); let value = - #crate_name::OutputValueType::resolve(msg, &ctx_selection_set).await?; + #crate_name::OutputValueType::resolve(msg, &ctx_selection_set, field.position).await?; let mut res = #crate_name::serde_json::Map::new(); res.insert(ctx_field.result_name().to_string(), value); return Ok(Some(res.into())); @@ -251,13 +251,11 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< #[#crate_name::async_trait::async_trait] impl #crate_name::SubscriptionType for SubscriptionRoot { fn create_type(field: &#crate_name::graphql_parser::query::Field, types: &mut std::collections::HashMap) -> #crate_name::Result<()> { - use #crate_name::ErrorWithPosition; #(#create_types)* - #crate_name::anyhow::bail!(#crate_name::QueryError::FieldNotFound { + Err(#crate_name::QueryError::FieldNotFound { field_name: field.name.clone(), object: #gql_typename.to_string(), - } - .with_position(field.position)); + }.into_error(field.position)) } async fn resolve( diff --git a/async-graphql-derive/src/union.rs b/async-graphql-derive/src/union.rs index 54c34b12..c986f9cb 100644 --- a/async-graphql-derive/src/union.rs +++ b/async-graphql-derive/src/union.rs @@ -98,31 +98,30 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #[#crate_name::async_trait::async_trait] impl #generics #crate_name::ObjectType for #ident #generics { async fn resolve_field(&self, ctx: &#crate_name::Context<'_>, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { - use #crate_name::ErrorWithPosition; - anyhow::bail!(#crate_name::QueryError::FieldNotFound { + Err(#crate_name::QueryError::FieldNotFound { field_name: field.name.clone(), object: #gql_typename.to_string(), - } - .with_position(field.position)); + }.into_error(field.position)) } fn collect_inline_fields<'a>( &'a self, name: &str, + pos: #crate_name::Pos, ctx: &#crate_name::ContextSelectionSet<'a>, futures: &mut Vec<#crate_name::BoxFieldFuture<'a>>, ) -> #crate_name::Result<()> { #(#collect_inline_fields)* - #crate_name::anyhow::bail!(#crate_name::QueryError::UnrecognizedInlineFragment { + Err(#crate_name::QueryError::UnrecognizedInlineFragment { object: #gql_typename.to_string(), name: name.to_string(), - }); + }.into_error(pos)) } } #[#crate_name::async_trait::async_trait] impl #generics #crate_name::OutputValueType for #ident #generics { - async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>, pos: #crate_name::Pos) -> #crate_name::Result<#crate_name::serde_json::Value> { #crate_name::do_resolve(ctx, value).await } } diff --git a/examples/error_extensions.rs b/examples/error_extensions.rs index acea145c..fe1af5be 100644 --- a/examples/error_extensions.rs +++ b/examples/error_extensions.rs @@ -1,3 +1,6 @@ +#[macro_use] +extern crate thiserror; + use actix_rt; use actix_web::{guard, web, App, HttpResponse, HttpServer}; use async_graphql::http::{graphiql_source, playground_source, GQLRequest, GQLResponse}; @@ -5,34 +8,26 @@ use async_graphql::*; use futures::TryFutureExt; use serde_json::json; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum MyError { + #[error("Could not find resource")] NotFound, + + #[error("ServerError")] ServerError(String), + + #[error("No Extensions")] ErrorWithoutExtensions, } -// Let's implement a mapping from our MyError to async_graphql::Error (anyhow::Error). -// But instead of mapping to async_graphql::Error directly we map to async_graphql::ExtendedError, -// which gives us the opportunity to provide custom extensions to our errors. -// Note: Values which can't get serialized to JSON-Objects simply get ignored. -impl From for Error { - fn from(my_error: MyError) -> Error { - match my_error { - MyError::NotFound => { - let msg = "Could not find resource".to_owned(); - let extensions = json!({"code": "NOT_FOUND"}); - ExtendedError(msg, extensions).into() +impl MyError { + fn extend_err(&self) -> serde_json::Value { + 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.") } - MyError::ServerError(reason) => { - ExtendedError("ServerError".to_owned(), json!({ "reason": reason })).into() - } - - MyError::ErrorWithoutExtensions => ExtendedError( - "No Extensions".to_owned(), - json!("This will be ignored since it does not represent an object."), - ) - .into(), } } } @@ -41,34 +36,39 @@ fn get_my_error() -> std::result::Result { Err(MyError::ServerError("The database is locked".to_owned())) } -struct QueryRoot {} +struct QueryRoot; #[Object] impl QueryRoot { #[field] - async fn do_not_find(&self) -> Result { - Err(MyError::NotFound)? + async fn do_not_find(&self) -> FieldResult { + Err(MyError::NotFound).extend_err(MyError::extend_err) } #[field] - async fn fail(&self) -> Result { - Ok(get_my_error()?) + async fn fail(&self) -> FieldResult { + Ok(get_my_error().extend_err(MyError::extend_err)?) } #[field] - async fn without_extensions(&self) -> Result { - Err(MyError::ErrorWithoutExtensions)? + async fn without_extensions(&self) -> FieldResult { + Err(MyError::ErrorWithoutExtensions).extend_err(MyError::extend_err)? } // 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 Display. + // Results where the error variant implements `std::error::Error`. #[field] - async fn parse_value(&self, val: String) -> Result { + async fn parse_value(&self, val: String) -> FieldResult { val.parse().extend_err(|err| { - json!({ "description": format!("Could not parse value {}: {}", val, err) }) + json!({ "description": format!("Could not parse value '{}': {}", val, err) }) }) } + + #[field] + async fn parse_value2(&self, val: String) -> FieldResult { + Ok(val.parse()?) + } } async fn index( @@ -99,7 +99,7 @@ async fn gql_graphiql() -> HttpResponse { async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() - .data(Schema::new(QueryRoot {}, EmptyMutation, EmptySubscription)) + .data(Schema::new(QueryRoot, EmptyMutation, EmptySubscription)) .service(web::resource("/").guard(guard::Post()).to(index)) .service(web::resource("/").guard(guard::Get()).to(gql_playgound)) .service( diff --git a/examples/starwars/model.rs b/examples/starwars/model.rs index b9da58de..2484bfb9 100644 --- a/examples/starwars/model.rs +++ b/examples/starwars/model.rs @@ -1,5 +1,5 @@ use super::StarWars; -use async_graphql::{Connection, Context, DataSource, EmptyEdgeFields, Result}; +use async_graphql::{Connection, Context, DataSource, EmptyEdgeFields, FieldResult}; #[async_graphql::Enum(desc = "One of the films in the Star Wars Trilogy")] pub enum Episode { @@ -118,7 +118,7 @@ impl QueryRoot { before: Option, first: Option, last: Option, - ) -> Result> { + ) -> FieldResult> { let humans = ctx .data::() .humans() @@ -149,7 +149,7 @@ impl QueryRoot { before: Option, first: Option, last: Option, - ) -> Result> { + ) -> FieldResult> { let droids = ctx .data::() .droids() diff --git a/src/base.rs b/src/base.rs index 648e5115..196cccaf 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,6 +1,7 @@ use crate::registry::Registry; -use crate::{registry, Context, ContextSelectionSet, QueryError, Result, ID}; +use crate::{registry, Context, ContextSelectionSet, Error, QueryError, Result, ID}; use graphql_parser::query::{Field, Value}; +use graphql_parser::Pos; use std::borrow::Cow; use std::future::Future; use std::pin::Pin; @@ -26,19 +27,15 @@ pub trait Type { } /// Parse `GlobalID`. - fn from_global_id(id: ID) -> Result { + fn from_global_id(id: ID) -> Option { let v: Vec<&str> = id.splitn(2, ':').collect(); if v.len() != 2 { - return Err(QueryError::InvalidGlobalID.into()); + return None; } if v[0] != Self::type_name() { - return Err(QueryError::InvalidGlobalIDType { - expect: Self::type_name().to_string(), - actual: v[0].to_string(), - } - .into()); + return None; } - Ok(v[1].to_string().into()) + Some(v[1].to_string().into()) } } @@ -52,7 +49,11 @@ pub trait InputValueType: Type + Sized { #[async_trait::async_trait] pub trait OutputValueType: Type { /// Resolve an output value to `serde_json::Value`. - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result; + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result; } #[allow(missing_docs)] @@ -75,13 +76,18 @@ pub trait ObjectType: OutputValueType { fn collect_inline_fields<'a>( &'a self, name: &str, + pos: Pos, _ctx: &ContextSelectionSet<'a>, _futures: &mut Vec>, ) -> Result<()> { - anyhow::bail!(QueryError::UnrecognizedInlineFragment { - object: Self::type_name().to_string(), - name: name.to_string(), - }); + Err(Error::Query { + pos, + path: None, + err: QueryError::UnrecognizedInlineFragment { + object: Self::type_name().to_string(), + name: name.to_string(), + }, + }) } } @@ -172,6 +178,7 @@ macro_rules! impl_scalar_internal { async fn resolve( value: &Self, _: &crate::ContextSelectionSet<'_>, + _pos: crate::Pos, ) -> crate::Result { value.to_json() } @@ -209,6 +216,7 @@ macro_rules! impl_scalar { async fn resolve( value: &Self, _: &async_graphql::ContextSelectionSet<'_>, + _pos: async_graphql::Pos, ) -> async_graphql::Result { value.to_json() } @@ -229,7 +237,11 @@ impl Type for &T { #[async_trait::async_trait] impl OutputValueType for &T { #[allow(clippy::trivially_copy_pass_by_ref)] - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { - T::resolve(*value, ctx).await + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result { + T::resolve(*value, ctx, pos).await } } diff --git a/src/context.rs b/src/context.rs index 86c67a6e..9ce33305 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,6 +1,6 @@ use crate::extensions::BoxExtension; use crate::registry::Registry; -use crate::{ErrorWithPosition, InputValueType, QueryError, Result, Type}; +use crate::{InputValueType, Pos, QueryError, Result, Type}; use bytes::Bytes; use graphql_parser::query::{ Directive, Field, FragmentDefinition, SelectionSet, Value, VariableDefinition, @@ -205,12 +205,25 @@ impl<'a> QueryPathNode<'a> { } f(&self.segment); } + + #[doc(hidden)] + pub fn to_json(&self) -> serde_json::Value { + let mut path: Vec = Vec::new(); + self.for_each(|segment| { + path.push(match segment { + QueryPathSegment::Index(idx) => (*idx).into(), + QueryPathSegment::Name(name) => (*name).to_string().into(), + }) + }); + path.into() + } } /// Query context #[derive(Clone)] pub struct ContextBase<'a, T> { - pub(crate) path_node: Option>, + #[allow(missing_docs)] + pub path_node: Option>, pub(crate) resolve_id: &'a AtomicUsize, pub(crate) extensions: &'a [BoxExtension], pub(crate) item: T, @@ -289,7 +302,7 @@ impl<'a, T> ContextBase<'a, T> { .expect("The specified data type does not exist.") } - fn var_value(&self, name: &str) -> Result { + fn var_value(&self, name: &str, pos: Pos) -> Result { let def = self .variable_definitions .iter() @@ -304,16 +317,16 @@ impl<'a, T> ContextBase<'a, T> { Err(QueryError::VarNotDefined { var_name: name.to_string(), } - .into()) + .into_error(pos)) } - fn resolve_input_value(&self, mut value: Value) -> Result { + fn resolve_input_value(&self, mut value: Value, pos: Pos) -> Result { match value { - Value::Variable(var_name) => self.var_value(&var_name), + Value::Variable(var_name) => self.var_value(&var_name, pos), Value::List(ref mut ls) => { for value in ls { if let Value::Variable(var_name) = value { - *value = self.var_value(&var_name)?; + *value = self.var_value(&var_name, pos)?; } } Ok(value) @@ -321,7 +334,7 @@ impl<'a, T> ContextBase<'a, T> { Value::Object(ref mut obj) => { for value in obj.values_mut() { if let Value::Variable(var_name) = value { - *value = self.var_value(&var_name)?; + *value = self.var_value(&var_name, pos)?; } } Ok(value) @@ -340,13 +353,13 @@ impl<'a, T> ContextBase<'a, T> { .find(|(name, _)| name == "if") .map(|(_, value)| value) { - let value = self.resolve_input_value(value.clone())?; + let value = self.resolve_input_value(value.clone(), directive.position)?; let res: bool = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: bool::qualified_type_name(), actual: value, } - .with_position(directive.position) + .into_error(directive.position) })?; if res { return Ok(true); @@ -357,8 +370,7 @@ impl<'a, T> ContextBase<'a, T> { arg_name: "if", arg_type: "Boolean!", } - .with_position(directive.position) - .into()); + .into_error(directive.position)); } } else if directive.name == "include" { if let Some(value) = directive @@ -367,13 +379,13 @@ impl<'a, T> ContextBase<'a, T> { .find(|(name, _)| name == "if") .map(|(_, value)| value) { - let value = self.resolve_input_value(value.clone())?; + let value = self.resolve_input_value(value.clone(), directive.position)?; let res: bool = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: bool::qualified_type_name(), actual: value, } - .with_position(directive.position) + .into_error(directive.position) })?; if !res { return Ok(true); @@ -384,15 +396,13 @@ impl<'a, T> ContextBase<'a, T> { arg_name: "if", arg_type: "Boolean!", } - .with_position(directive.position) - .into()); + .into_error(directive.position)); } } else { return Err(QueryError::UnknownDirective { name: directive.name.clone(), } - .with_position(directive.position) - .into()); + .into_error(directive.position)); } } @@ -426,6 +436,7 @@ impl<'a> ContextBase<'a, &'a Field> { pub fn param_value Value>( &self, name: &str, + pos: Pos, default: F, ) -> Result { match self @@ -436,13 +447,13 @@ impl<'a> ContextBase<'a, &'a Field> { .cloned() { Some(value) => { - let value = self.resolve_input_value(value)?; + let value = self.resolve_input_value(value, pos)?; let res = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: T::qualified_type_name(), actual: value, } - .with_position(self.item.position) + .into_error(pos) })?; Ok(res) } @@ -453,7 +464,7 @@ impl<'a> ContextBase<'a, &'a Field> { expect: T::qualified_type_name(), actual: value.clone(), } - .with_position(self.item.position) + .into_error(pos) })?; Ok(res) } diff --git a/src/error.rs b/src/error.rs index bc55a792..9834feb8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,77 @@ -use crate::Error; -use graphql_parser::query::Value; +use graphql_parser::query::{ParseError, Value}; use graphql_parser::Pos; -use std::fmt::{Debug, Display, Formatter}; +use std::error::Error as StdError; +use std::fmt::Debug; -/// Error for query parser -#[derive(Debug, Error)] -#[error("{0}")] -pub struct QueryParseError(pub(crate) String); +/// FieldError type +pub struct FieldError(anyhow::Error, Option); + +impl FieldError { + #[doc(hidden)] + pub fn into_error(self, pos: Pos) -> Error { + Error::Query { + pos, + path: None, + err: QueryError::FieldError { + err: self.0, + extended_error: self.1, + }, + } + } + + #[doc(hidden)] + pub fn into_error_with_path(self, pos: Pos, path: serde_json::Value) -> Error { + Error::Query { + pos, + path: Some(path), + err: QueryError::FieldError { + err: self.0, + extended_error: self.1, + }, + } + } +} + +/// FieldResult type +pub type FieldResult = std::result::Result; + +impl From for FieldError +where + E: StdError + Send + Sync + 'static, +{ + fn from(err: E) -> Self { + FieldError(anyhow::Error::from(err), None) + } +} + +#[allow(missing_docs)] +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; +} + +impl ResultExt for std::result::Result +where + E: StdError + 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))) + } + Ok(value) => Ok(value), + } + } +} /// Error for query #[derive(Debug, Error)] @@ -131,128 +196,57 @@ pub enum QueryError { name: String, }, - #[error("Argument \"{field_name}\" must be a non-negative integer")] - ArgumentMustBeNonNegative { - /// Field name - field_name: String, - }, - - #[error("Invalid global id")] - InvalidGlobalID, - - #[error("Invalid global id, expected type \"{expect}\", found {actual}.")] - InvalidGlobalIDType { - /// Expect type - expect: String, - - /// Actual type - actual: String, - }, - - #[error("Too complex.")] + #[error("Too complex")] TooComplex, - #[error("Too deep.")] + #[error("Too deep")] TooDeep, + + #[error("Failed to resolve field: {err}")] + FieldError { + err: anyhow::Error, + extended_error: Option, + }, } -/// Creates a wrapper with an error location -#[allow(missing_docs)] -pub trait ErrorWithPosition { - type Result; - - fn with_position(self, position: Pos) -> PositionError; -} - -impl> ErrorWithPosition for T { - type Result = PositionError; - - fn with_position(self, position: Pos) -> PositionError { - PositionError { - position, - inner: self.into(), +impl QueryError { + #[doc(hidden)] + pub fn into_error(self, pos: Pos) -> Error { + Error::Query { + pos, + path: None, + err: self, } } } -/// A wrapper with the wrong location -#[allow(missing_docs)] -#[derive(Debug, Error)] -pub struct PositionError { - pub position: Pos, - pub inner: Error, -} - -impl PositionError { - #[allow(missing_docs)] - pub fn new(position: Pos, inner: Error) -> Self { - Self { position, inner } - } - - #[allow(missing_docs)] - pub fn into_inner(self) -> Error { - self.inner - } -} - -impl Display for PositionError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner) - } -} - #[derive(Debug)] pub struct RuleError { pub locations: Vec, pub message: String, } -#[derive(Debug, Error)] -pub struct RuleErrors { - pub errors: Vec, -} - -impl Display for RuleErrors { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for error in &self.errors { - writeln!(f, "{}", error.message)?; +impl From for Error { + fn from(err: ParseError) -> Self { + Error::Parse { + message: err.to_string(), } - Ok(()) - } -} - -/// A wrapped Error with extensions. -#[derive(Debug, Error)] -pub struct ExtendedError(pub String, pub serde_json::Value); - -impl Display for ExtendedError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) } } #[allow(missing_docs)] -pub trait ResultExt -where - Self: Sized, - E: std::fmt::Display + Sized, -{ - fn extend_err(self, cb: CB) -> crate::Result - where - CB: FnOnce(&E) -> serde_json::Value; -} +#[derive(Debug, Error)] +pub enum Error { + #[error("Parse error: {message}")] + Parse { message: String }, -impl ResultExt for std::result::Result -where - E: std::fmt::Display + Sized, -{ - fn extend_err(self, cb: C) -> crate::Result - where - C: FnOnce(&E) -> serde_json::Value, - { - match self { - Err(e) => Err(anyhow::anyhow!(ExtendedError(e.to_string(), cb(&e)))), - Ok(value) => Ok(value), - } - } + #[error("Query error: {err}")] + Query { + pos: Pos, + path: Option, + err: QueryError, + }, + + #[error("Rule error")] + Rule { errors: Vec }, } diff --git a/src/extensions/tracing.rs b/src/extensions/tracing.rs index 4df064f7..bd45a807 100644 --- a/src/extensions/tracing.rs +++ b/src/extensions/tracing.rs @@ -1,5 +1,4 @@ use crate::extensions::{Extension, ResolveInfo}; -use crate::QueryPathSegment; use chrono::{DateTime, Utc}; use parking_lot::Mutex; use serde::ser::SerializeMap; @@ -91,16 +90,7 @@ impl Extension for ApolloTracing { inner.pending_resolves.insert( info.resolve_id, PendingResolve { - path: { - let mut path: Vec = Vec::new(); - info.path_node.for_each(|segment| { - path.push(match segment { - QueryPathSegment::Index(idx) => (*idx).into(), - QueryPathSegment::Name(name) => (*name).to_string().into(), - }) - }); - path.into() - }, + path: info.path_node.to_json(), field_name: info.path_node.field_name().to_string(), parent_type: info.parent_type.to_string(), return_type: info.return_type.to_string(), diff --git a/src/http/mod.rs b/src/http/mod.rs index 9a82c0ef..13999f32 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,15 +6,13 @@ mod playground_source; pub use graphiql_source::graphiql_source; pub use playground_source::playground_source; -use crate::error::{ExtendedError, RuleError, RuleErrors}; use crate::{ - ObjectType, PositionError, QueryBuilder, QueryResult, Result, Schema, SubscriptionType, + Error, ObjectType, QueryBuilder, QueryError, QueryResponse, Result, Schema, SubscriptionType, Variables, }; use graphql_parser::Pos; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Serialize, Serializer}; -use std::ops::Deref; /// GraphQL Request object #[derive(Deserialize, Clone, PartialEq, Debug)] @@ -55,7 +53,7 @@ impl GQLRequest { } /// Serializable query result type -pub struct GQLResponse(pub Result); +pub struct GQLResponse(pub Result); impl Serialize for GQLResponse { fn serialize(&self, serializer: S) -> std::result::Result { @@ -81,45 +79,62 @@ impl Serialize for GQLResponse { } /// Serializable error type -pub struct GQLError<'a>(pub &'a anyhow::Error); - -impl<'a> Deref for GQLError<'a> { - type Target = anyhow::Error; - - fn deref(&self) -> &Self::Target { - self.0 - } -} +pub struct GQLError<'a>(pub &'a Error); impl<'a> Serialize for GQLError<'a> { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { - if let Some(err) = self.0.downcast_ref::() { - let mut seq = serializer.serialize_seq(Some(1))?; - seq.serialize_element(&GQLPositionError(err))?; - seq.end() - } else if let Some(err) = self.0.downcast_ref::() { - let mut seq = serializer.serialize_seq(Some(err.errors.len()))?; - for err in &err.errors { - seq.serialize_element(&GQLRuleError(err))?; - } - seq.end() - } else { - let mut seq = serializer.serialize_seq(None)?; - if let Some(extensions) = get_error_extensions(self.0) { - seq.serialize_element(&serde_json::json!({ - "message": self.0.to_string(), - "extensions": extensions - }))?; - } else { - seq.serialize_element(&serde_json::json!({ - "message": self.0.to_string(), + match self.0 { + Error::Parse { message } => { + let mut seq = serializer.serialize_seq(Some(1))?; + seq.serialize_element(&serde_json::json! ({ + "message": message, }))?; + seq.end() } + Error::Query { pos, path, err } => { + let mut seq = serializer.serialize_seq(Some(1))?; + if let QueryError::FieldError { + err, + extended_error, + } = err + { + let mut map = serde_json::Map::new(); - seq.end() + map.insert("message".to_string(), err.to_string().into()); + map.insert( + "locations".to_string(), + serde_json::json!([{"line": pos.line, "column": pos.column}]), + ); + + if let Some(path) = path { + map.insert("path".to_string(), path.clone()); + } + + 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!({ + "message": err.to_string(), + "locations": [{"line": pos.line, "column": pos.column}] + }))?; + } + seq.end() + } + Error::Rule { errors } => { + let mut seq = serializer.serialize_seq(Some(1))?; + for error in errors { + seq.serialize_element(&serde_json::json!({ + "message": error.message, + "locations": error.locations.iter().map(|pos| serde_json::json!({"line": pos.line, "column": pos.column})).collect::>(), + }))?; + } + seq.end() + } } } } @@ -138,52 +153,9 @@ impl<'a> Serialize for GQLErrorPos<'a> { } } -struct GQLPositionError<'a>(&'a PositionError); - -impl<'a> Serialize for GQLPositionError<'a> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(None)?; - map.serialize_entry("message", &self.0.inner.to_string())?; - map.serialize_entry( - "locations", - std::slice::from_ref(&GQLErrorPos(&self.0.position)), - )?; - if let Some(extensions) = get_error_extensions(&self.0.inner) { - map.serialize_entry("extensions", &extensions)?; - } - map.end() - } -} - -struct GQLRuleError<'a>(&'a RuleError); - -impl<'a> Serialize for GQLRuleError<'a> { - fn serialize(&self, serializer: S) -> std::result::Result - where - S: Serializer, - { - let mut map = serializer.serialize_map(None)?; - map.serialize_entry("message", &self.0.message)?; - map.serialize_entry( - "locations", - &self - .0 - .locations - .iter() - .map(|pos| GQLErrorPos(pos)) - .collect::>(), - )?; - map.end() - } -} - #[cfg(test)] mod tests { use super::*; - use crate::ErrorWithPosition; use graphql_parser::Pos; use serde_json::json; @@ -235,7 +207,7 @@ mod tests { #[test] fn test_response_data() { - let resp = GQLResponse(Ok(QueryResult { + let resp = GQLResponse(Ok(QueryResponse { data: json!({"ok": true}), extensions: None, })); @@ -250,13 +222,20 @@ mod tests { } #[test] - fn test_response_error_with_extension() { - let err = ExtendedError( - "MyErrorMessage".to_owned(), - json!({ - "code": "MY_TEST_CODE" - }), - ); + fn test_field_error_with_extension() { + let err = Error::Query { + pos: Pos { + line: 10, + column: 20, + }, + path: None, + err: QueryError::FieldError { + err: anyhow::anyhow!("MyErrorMessage"), + extended_error: Some(json!({ + "code": "MY_TEST_CODE" + })), + }, + }; let resp = GQLResponse(Err(err.into())); @@ -267,20 +246,8 @@ mod tests { "message":"MyErrorMessage", "extensions": { "code": "MY_TEST_CODE" - } - }] - }) - ); - } - - #[test] - fn test_response_error() { - let resp = GQLResponse(Err(anyhow::anyhow!("error"))); - assert_eq!( - serde_json::to_value(resp).unwrap(), - json!({ - "errors": [{ - "message":"error" + }, + "locations": [{"line": 10, "column": 20}] }] }) ); @@ -288,17 +255,19 @@ mod tests { #[test] fn test_response_error_with_pos() { - let resp = GQLResponse(Err(anyhow::anyhow!("error") - .with_position(Pos { + let resp = GQLResponse(Err(Error::Query { + pos: Pos { line: 10, column: 20, - }) - .into())); + }, + path: None, + err: QueryError::NotSupported, + })); assert_eq!( serde_json::to_value(resp).unwrap(), json!({ "errors": [{ - "message":"error", + "message":"Not supported.", "locations": [ {"line": 10, "column": 20} ] @@ -307,12 +276,3 @@ mod tests { ); } } - -fn get_error_extensions(err: &crate::Error) -> Option<&serde_json::Value> { - if let Some(extended_err) = err.downcast_ref::() { - if extended_err.1.is_object() { - return Some(&extended_err.1); - } - } - None -} diff --git a/src/lib.rs b/src/lib.rs index e46ee797..2cada826 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,11 +100,10 @@ pub mod http; pub use base::{Scalar, Type}; pub use context::{Context, QueryPathSegment, Variables}; -pub use error::{ - ErrorWithPosition, ExtendedError, PositionError, QueryError, QueryParseError, ResultExt, -}; +pub use error::{Error, FieldError, FieldResult, QueryError, ResultExt}; pub use graphql_parser::query::Value; -pub use query::{QueryBuilder, QueryResult}; +pub use graphql_parser::Pos; +pub use query::{QueryBuilder, QueryResponse}; pub use registry::CacheControl; pub use scalars::ID; pub use schema::{publish, Schema}; @@ -118,10 +117,7 @@ pub use types::{ }; /// Result type, are actually `anyhow::Result` -pub type Result = anyhow::Result; - -/// Error type, are actually `anyhow::Error` -pub type Error = anyhow::Error; +pub type Result = std::result::Result; // internal types #[doc(hidden)] @@ -176,7 +172,7 @@ pub use types::{EnumItem, EnumType}; /// - Option, such as `Option` /// - Object and &Object /// - Enum -/// - Result, such as `Result` +/// - FieldResult, such as `FieldResult` /// /// # Context /// @@ -211,7 +207,7 @@ pub use types::{EnumItem, EnumType}; /// } /// /// #[field(desc = "value with error")] -/// async fn value_with_error(&self) -> Result { +/// async fn value_with_error(&self) -> FieldResult { /// Ok(self.value) /// } /// diff --git a/src/query.rs b/src/query.rs index aba40fe6..ddf9f45e 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,18 +1,19 @@ use crate::context::Data; use crate::extensions::BoxExtension; use crate::registry::CacheControl; -use crate::{ContextBase, OutputValueType, Result, Schema}; +use crate::{ContextBase, Error, OutputValueType, Result, Schema}; use crate::{ObjectType, QueryError, Variables}; use bytes::Bytes; use graphql_parser::query::{ Definition, Document, OperationDefinition, SelectionSet, VariableDefinition, }; +use graphql_parser::Pos; use std::any::Any; use std::collections::HashMap; use std::sync::atomic::AtomicUsize; -/// Query result -pub struct QueryResult { +/// Query response +pub struct QueryResponse { /// Data of query result pub data: serde_json::Value, @@ -126,16 +127,19 @@ impl QueryBuilder } /// Execute the query. - pub async fn execute(self) -> Result + pub async fn execute(self) -> Result where Query: ObjectType + Send + Sync, Mutation: ObjectType + Send + Sync, { let resolve_id = AtomicUsize::default(); let mut fragments = HashMap::new(); - let (selection_set, variable_definitions, is_query) = self - .current_operation() - .ok_or_else(|| QueryError::MissingOperation)?; + let (selection_set, variable_definitions, is_query) = + self.current_operation().ok_or_else(|| Error::Query { + pos: Pos::default(), + path: None, + err: QueryError::MissingOperation, + })?; for definition in &self.document.definitions { if let Definition::Fragment(fragment) = &definition { @@ -158,13 +162,13 @@ impl QueryBuilder self.extensions.iter().for_each(|e| e.execution_start()); let data = if is_query { - OutputValueType::resolve(&self.schema.0.query, &ctx).await? + OutputValueType::resolve(&self.schema.0.query, &ctx, selection_set.span.0).await? } else { - OutputValueType::resolve(&self.schema.0.mutation, &ctx).await? + OutputValueType::resolve(&self.schema.0.mutation, &ctx, selection_set.span.0).await? }; self.extensions.iter().for_each(|e| e.execution_end()); - let res = QueryResult { + let res = QueryResponse { data, extensions: if !self.extensions.is_empty() { Some( diff --git a/src/resolver.rs b/src/resolver.rs index c81f3260..31047267 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,6 +1,6 @@ use crate::base::BoxFieldFuture; use crate::extensions::ResolveInfo; -use crate::{ContextSelectionSet, Error, ErrorWithPosition, ObjectType, QueryError, Result}; +use crate::{ContextSelectionSet, Error, ObjectType, QueryError, Result}; use futures::{future, TryFutureExt}; use graphql_parser::query::{Selection, TypeCondition}; use std::iter::FromIterator; @@ -24,10 +24,13 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( futures: &mut Vec>, ) -> Result<()> { if ctx.items.is_empty() { - anyhow::bail!(QueryError::MustHaveSubFields { - object: T::type_name().to_string(), - } - .with_position(ctx.span.0)); + return Err(Error::Query { + pos: ctx.span.0, + path: None, + err: QueryError::MustHaveSubFields { + object: T::type_name().to_string(), + }, + }); } for selection in &ctx.item.items { @@ -69,11 +72,14 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( { Some(ty) => &ty, None => { - anyhow::bail!(QueryError::FieldNotFound { - field_name: field.name.clone(), - object: T::type_name().to_string(), - } - .with_position(field.position)); + return Err(Error::Query { + pos: field.position, + path: None, + err: QueryError::FieldNotFound { + field_name: field.name.clone(), + object: T::type_name().to_string(), + }, + }); } }, }; @@ -112,10 +118,13 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( futures, )?; } else { - return Err(QueryError::UnknownFragment { - name: fragment_spread.fragment_name.clone(), - } - .into()); + return Err(Error::Query { + pos: fragment_spread.position, + path: None, + err: QueryError::UnknownFragment { + name: fragment_spread.fragment_name.clone(), + }, + }); } } Selection::InlineFragment(inline_fragment) => { @@ -126,6 +135,7 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( if let Some(TypeCondition::On(name)) = &inline_fragment.type_condition { root.collect_inline_fields( name, + inline_fragment.position, &ctx.with_selection_set(&inline_fragment.selection_set), futures, )?; diff --git a/src/scalars/string.rs b/src/scalars/string.rs index 2039746e..bbe56249 100644 --- a/src/scalars/string.rs +++ b/src/scalars/string.rs @@ -1,6 +1,6 @@ use crate::{ - impl_scalar_internal, registry, ContextSelectionSet, OutputValueType, Result, Scalar, Type, - Value, + impl_scalar_internal, registry, ContextSelectionSet, OutputValueType, Pos, Result, Scalar, + Type, Value, }; use std::borrow::Cow; @@ -55,7 +55,11 @@ impl<'a> Type for &'a str { #[async_trait::async_trait] impl<'a> OutputValueType for &'a str { - async fn resolve(value: &Self, _: &ContextSelectionSet<'_>) -> Result { + async fn resolve( + value: &Self, + _: &ContextSelectionSet<'_>, + _pos: Pos, + ) -> Result { Ok((*value).into()) } } diff --git a/src/schema.rs b/src/schema.rs index f506ae38..946395ea 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -7,7 +7,7 @@ use crate::subscription::{SubscriptionConnectionBuilder, SubscriptionStub, Subsc use crate::types::QueryRoot; use crate::validation::{check_rules, CheckResult}; use crate::{ - ContextSelectionSet, ObjectType, QueryError, QueryParseError, Result, SubscriptionType, Type, + ContextSelectionSet, Error, ObjectType, Pos, QueryError, Result, SubscriptionType, Type, Variables, }; use futures::channel::mpsc; @@ -219,7 +219,7 @@ where .map(|factory| factory()) .collect::>(); extensions.iter().for_each(|e| e.parse_start(source)); - let document = parse_query(source).map_err(|err| QueryParseError(err.to_string()))?; + let document = parse_query(source).map_err(Into::::into)?; extensions.iter().for_each(|e| e.parse_end()); extensions.iter().for_each(|e| e.validation_start()); @@ -232,13 +232,13 @@ where if let Some(limit_complexity) = self.0.complexity { if complexity > limit_complexity { - return Err(QueryError::TooComplex.into()); + return Err(QueryError::TooComplex.into_error(Pos { line: 0, column: 0 })); } } if let Some(limit_depth) = self.0.depth { if depth > limit_depth { - return Err(QueryError::TooDeep.into()); + return Err(QueryError::TooDeep.into_error(Pos { line: 0, column: 0 })); } } @@ -263,7 +263,7 @@ where where Self: Sized, { - let document = parse_query(source).map_err(|err| QueryParseError(err.to_string()))?; + let document = parse_query(source).map_err(Into::::into)?; check_rules(&self.0.registry, &document)?; let mut fragments = HashMap::new(); @@ -288,8 +288,9 @@ where QueryError::UnknownOperationNamed { name: name.to_string(), } + .into_error(Pos::default()) } else { - QueryError::MissingOperation + QueryError::MissingOperation.into_error(Pos::default()) })?; let mut types = HashMap::new(); @@ -358,7 +359,7 @@ fn create_subscription_types( return Err(QueryError::UnknownFragment { name: fragment_spread.fragment_name.clone(), } - .into()); + .into_error(fragment_spread.position)); } } Selection::InlineFragment(inline_fragment) => { diff --git a/src/subscription/connection.rs b/src/subscription/connection.rs index 064570c2..bfb2db5a 100644 --- a/src/subscription/connection.rs +++ b/src/subscription/connection.rs @@ -34,6 +34,9 @@ impl SubscriptionStubs, stubs: &mut SubscriptionStubs, data: Bytes, - ) -> Result> + ) -> std::result::Result, Self::Error> where Query: ObjectType + Sync + Send + 'static, Mutation: ObjectType + Sync + Send + 'static, diff --git a/src/subscription/ws_transport.rs b/src/subscription/ws_transport.rs index 9e8b39ef..b1a16b50 100644 --- a/src/subscription/ws_transport.rs +++ b/src/subscription/ws_transport.rs @@ -1,6 +1,6 @@ use crate::http::{GQLError, GQLRequest, GQLResponse}; use crate::{ - ObjectType, QueryResult, Result, Schema, SubscriptionStubs, SubscriptionTransport, + ObjectType, QueryResponse, Result, Schema, SubscriptionStubs, SubscriptionTransport, SubscriptionType, Variables, }; use bytes::Bytes; @@ -22,12 +22,14 @@ pub struct WebSocketTransport { } impl SubscriptionTransport for WebSocketTransport { + type Error = String; + fn handle_request( &mut self, schema: &Schema, stubs: &mut SubscriptionStubs, data: Bytes, - ) -> Result> + ) -> std::result::Result, Self::Error> where Query: ObjectType + Sync + Send + 'static, Mutation: ObjectType + Sync + Send + 'static, @@ -92,10 +94,10 @@ impl SubscriptionTransport for WebSocketTransport { } Ok(None) } - "connection_terminate" => Err(anyhow::anyhow!("connection_terminate")), - _ => Err(anyhow::anyhow!("unknown op")), + "connection_terminate" => Err("connection_terminate".to_string()), + _ => Err("unknown op".to_string()), }, - Err(err) => Err(err.into()), + Err(err) => Err(err.to_string()), } } @@ -106,7 +108,7 @@ impl SubscriptionTransport for WebSocketTransport { ty: "data".to_string(), id: Some(id.clone()), payload: Some( - serde_json::to_value(GQLResponse(result.map(|data| QueryResult { + serde_json::to_value(GQLResponse(result.map(|data| QueryResponse { data, extensions: None, }))) diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index c9ea32f8..3fc23050 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -1,8 +1,8 @@ use crate::types::connection::edge::Edge; use crate::types::connection::page_info::PageInfo; use crate::{ - do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, - OutputValueType, QueryError, Result, Type, + do_resolve, registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, Pos, + QueryError, Result, Type, }; use graphql_parser::query::Field; use inflector::Inflector; @@ -131,9 +131,7 @@ impl ObjectType if field.name.as_str() == "pageInfo" { let ctx_obj = ctx.with_selection_set(&field.selection_set); let page_info = &self.page_info; - return OutputValueType::resolve(page_info, &ctx_obj) - .await - .map_err(|err| err.with_position(field.position).into()); + return OutputValueType::resolve(page_info, &ctx_obj, field.position).await; } else if field.name.as_str() == "edges" { let ctx_obj = ctx.with_selection_set(&field.selection_set); let edges = self @@ -145,9 +143,7 @@ impl ObjectType node, }) .collect::>(); - return OutputValueType::resolve(&edges, &ctx_obj) - .await - .map_err(|err| err.with_position(field.position).into()); + return OutputValueType::resolve(&edges, &ctx_obj, field.position).await; } else if field.name.as_str() == "totalCount" { return Ok(self .total_count @@ -160,16 +156,17 @@ impl ObjectType .iter() .map(|(_, _, item)| item) .collect::>(); - return OutputValueType::resolve(&items, &ctx_obj) - .await - .map_err(|err| err.with_position(field.position).into()); + return OutputValueType::resolve(&items, &ctx_obj, field.position).await; } - anyhow::bail!(QueryError::FieldNotFound { - field_name: field.name.clone(), - object: Connection::::type_name().to_string(), - } - .with_position(field.position)) + Err(Error::Query { + pos: field.position, + path: None, + err: QueryError::FieldNotFound { + field_name: field.name.clone(), + object: Connection::::type_name().to_string(), + }, + }) } } @@ -177,7 +174,11 @@ impl ObjectType impl OutputValueType for Connection { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + _pos: Pos, + ) -> Result { do_resolve(ctx, value).await } } diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index 4f678db5..78ac6d21 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -1,8 +1,8 @@ use crate::{ - do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, - OutputValueType, Result, Type, + do_resolve, registry, Context, ContextSelectionSet, ObjectType, OutputValueType, Result, Type, }; use graphql_parser::query::Field; +use graphql_parser::Pos; use std::borrow::Cow; use std::collections::HashMap; @@ -81,9 +81,7 @@ where async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result { if field.name.as_str() == "node" { let ctx_obj = ctx.with_selection_set(&field.selection_set); - return OutputValueType::resolve(self.node, &ctx_obj) - .await - .map_err(|err| err.with_position(field.position).into()); + return OutputValueType::resolve(self.node, &ctx_obj, field.position).await; } else if field.name.as_str() == "cursor" { return Ok(self.cursor.into()); } @@ -98,7 +96,11 @@ where T: OutputValueType + Send + Sync + 'a, E: ObjectType + Sync + Send + 'a, { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + _pos: Pos, + ) -> Result { do_resolve(ctx, value).await } } diff --git a/src/types/connection/mod.rs b/src/types/connection/mod.rs index 33cb121d..001f6e53 100644 --- a/src/types/connection/mod.rs +++ b/src/types/connection/mod.rs @@ -3,7 +3,7 @@ mod edge; mod page_info; mod slice; -use crate::{Context, ObjectType, QueryError, Result}; +use crate::{Context, FieldResult, ObjectType}; pub use connection_type::Connection; @@ -65,7 +65,7 @@ impl EmptyEdgeFields {} /// type Element = i32; /// type EdgeFieldsObj = DiffFields; /// -/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> Result> { +/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> FieldResult> { /// let (start, end) = match operation { /// QueryOperation::Forward {after, limit} => { /// let start = after.and_then(|after| base64::decode(after).ok()) @@ -97,7 +97,7 @@ impl EmptyEdgeFields {} /// before: Option, /// first: Option, /// last: Option -/// ) -> Result> { +/// ) -> FieldResult> { /// Numbers.query(ctx, after, before, first, last).await /// } /// } @@ -138,26 +138,17 @@ pub trait DataSource: Sync + Send { /// Execute the query. async fn query( &self, - ctx: &Context<'_>, + _ctx: &Context<'_>, after: Option, before: Option, first: Option, last: Option, - ) -> Result> { + ) -> FieldResult> { let operation = if let Some(after) = &after { QueryOperation::Forward { after: Some(after), limit: match first { - Some(value) => { - if value < 0 { - return Err(QueryError::ArgumentMustBeNonNegative { - field_name: ctx.name.clone(), - } - .into()); - } else { - value as usize - } - } + Some(value) => value.max(0) as usize, None => 10, }, } @@ -165,42 +156,19 @@ pub trait DataSource: Sync + Send { QueryOperation::Backward { before: Some(before), limit: match last { - Some(value) => { - if value < 0 { - return Err(QueryError::ArgumentMustBeNonNegative { - field_name: ctx.name.clone(), - } - .into()); - } else { - value as usize - } - } + Some(value) => value.max(0) as usize, None => 10, }, } } else if let Some(first) = first { QueryOperation::Forward { after: None, - limit: if first < 0 { - return Err(QueryError::ArgumentMustBeNonNegative { - field_name: ctx.name.clone(), - } - .into()); - } else { - first as usize - }, + limit: first.max(0) as usize, } } else if let Some(last) = last { QueryOperation::Backward { before: None, - limit: if last < 0 { - return Err(QueryError::ArgumentMustBeNonNegative { - field_name: ctx.name.clone(), - } - .into()); - } else { - last as usize - }, + limit: last.max(0) as usize, } } else { QueryOperation::Forward { @@ -216,5 +184,5 @@ pub trait DataSource: Sync + Send { async fn query_operation( &self, operation: &QueryOperation<'_>, - ) -> Result>; + ) -> FieldResult>; } diff --git a/src/types/connection/slice.rs b/src/types/connection/slice.rs index 8f884b90..17b04d5c 100644 --- a/src/types/connection/slice.rs +++ b/src/types/connection/slice.rs @@ -1,5 +1,5 @@ use crate::types::connection::{EmptyEdgeFields, QueryOperation}; -use crate::{Connection, DataSource, Result}; +use crate::{Connection, DataSource, FieldResult}; use byteorder::{ReadBytesExt, BE}; #[async_trait::async_trait] @@ -10,7 +10,7 @@ impl<'a, T: Sync> DataSource for &'a [T] { async fn query_operation( &self, operation: &QueryOperation<'_>, - ) -> Result> { + ) -> FieldResult> { let (start, end) = match operation { QueryOperation::Forward { after, limit } => { let start = after diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 04e64508..618c5d58 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -1,7 +1,9 @@ use crate::{ - registry, Context, ContextSelectionSet, ObjectType, OutputValueType, QueryError, Result, Type, + registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, QueryError, Result, + Type, }; use graphql_parser::query::Field; +use graphql_parser::Pos; use std::borrow::Cow; /// Empty mutation @@ -52,7 +54,15 @@ impl ObjectType for EmptyMutation { #[async_trait::async_trait] impl OutputValueType for EmptyMutation { - async fn resolve(_value: &Self, _ctx: &ContextSelectionSet<'_>) -> Result { - Err(QueryError::NotConfiguredMutations.into()) + async fn resolve( + _value: &Self, + _ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result { + Err(Error::Query { + pos, + path: None, + err: QueryError::NotConfiguredMutations, + }) } } diff --git a/src/types/empty_subscription.rs b/src/types/empty_subscription.rs index 2bd35dad..77864c4f 100644 --- a/src/types/empty_subscription.rs +++ b/src/types/empty_subscription.rs @@ -1,8 +1,9 @@ use crate::{ - registry, ContextBase, ContextSelectionSet, OutputValueType, QueryError, Result, + registry, ContextBase, ContextSelectionSet, Error, OutputValueType, QueryError, Result, SubscriptionType, Type, }; use graphql_parser::query::Field; +use graphql_parser::Pos; use serde_json::Value; use std::any::{Any, TypeId}; use std::borrow::Cow; @@ -51,7 +52,15 @@ impl SubscriptionType for EmptySubscription { #[async_trait::async_trait] impl OutputValueType for EmptySubscription { - async fn resolve(_value: &Self, _ctx: &ContextSelectionSet<'_>) -> Result { - Err(QueryError::NotConfiguredSubscriptions.into()) + async fn resolve( + _value: &Self, + _ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result { + Err(Error::Query { + pos, + path: None, + err: QueryError::NotConfiguredSubscriptions, + }) } } diff --git a/src/types/list.rs b/src/types/list.rs index 0be999ca..4fdec7a4 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,4 +1,5 @@ use crate::{registry, ContextSelectionSet, InputValueType, OutputValueType, Result, Type, Value}; +use graphql_parser::Pos; use std::borrow::Cow; impl Type for Vec { @@ -34,11 +35,15 @@ impl InputValueType for Vec { #[allow(clippy::ptr_arg)] #[async_trait::async_trait] impl OutputValueType for Vec { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result { let mut futures = Vec::with_capacity(value.len()); for (idx, item) in value.iter().enumerate() { let ctx_idx = ctx.with_index(idx); - futures.push(async move { OutputValueType::resolve(item, &ctx_idx).await }); + futures.push(async move { OutputValueType::resolve(item, &ctx_idx, pos).await }); } Ok(futures::future::try_join_all(futures).await?.into()) } @@ -56,11 +61,15 @@ impl Type for &[T] { #[async_trait::async_trait] impl OutputValueType for &[T] { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result { let mut futures = Vec::with_capacity(value.len()); for (idx, item) in (*value).iter().enumerate() { let ctx_idx = ctx.with_index(idx); - futures.push(async move { OutputValueType::resolve(item, &ctx_idx).await }); + futures.push(async move { OutputValueType::resolve(item, &ctx_idx, pos).await }); } Ok(futures::future::try_join_all(futures).await?.into()) } diff --git a/src/types/optional.rs b/src/types/optional.rs index f5a73644..d680add7 100644 --- a/src/types/optional.rs +++ b/src/types/optional.rs @@ -1,4 +1,6 @@ -use crate::{registry, ContextSelectionSet, InputValueType, OutputValueType, Result, Type, Value}; +use crate::{ + registry, ContextSelectionSet, InputValueType, OutputValueType, Pos, Result, Type, Value, +}; use std::borrow::Cow; impl Type for Option { @@ -27,10 +29,13 @@ impl InputValueType for Option { #[async_trait::async_trait] impl OutputValueType for Option { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result where - { + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + pos: Pos, + ) -> Result where { if let Some(inner) = value { - OutputValueType::resolve(inner, ctx).await + OutputValueType::resolve(inner, ctx, pos).await } else { Ok(serde_json::Value::Null) } diff --git a/src/types/query_root.rs b/src/types/query_root.rs index a130dbfd..54d0fec1 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -1,9 +1,10 @@ use crate::model::{__Schema, __Type}; use crate::{ - do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, - OutputValueType, QueryError, Result, Type, Value, + do_resolve, registry, Context, ContextSelectionSet, Error, ObjectType, OutputValueType, + QueryError, Result, Type, Value, }; use graphql_parser::query::Field; +use graphql_parser::Pos; use std::borrow::Cow; use std::collections::HashMap; @@ -69,11 +70,14 @@ impl ObjectType for QueryRoot { async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result { if field.name.as_str() == "__schema" { if self.disable_introspection { - return Err(QueryError::FieldNotFound { - field_name: field.name.clone(), - object: Self::type_name().to_string(), - } - .into()); + return Err(Error::Query { + pos: field.position, + path: Some(ctx.path_node.as_ref().unwrap().to_json()), + err: QueryError::FieldNotFound { + field_name: field.name.clone(), + object: Self::type_name().to_string(), + }, + }); } let ctx_obj = ctx.with_selection_set(&field.selection_set); @@ -82,11 +86,11 @@ impl ObjectType for QueryRoot { registry: &ctx.registry, }, &ctx_obj, + field.position, ) - .await - .map_err(|err| err.with_position(field.position).into()); + .await; } else if field.name.as_str() == "__type" { - let type_name: String = ctx.param_value("name", || Value::Null)?; + let type_name: String = ctx.param_value("name", field.position, || Value::Null)?; let ctx_obj = ctx.with_selection_set(&field.selection_set); return OutputValueType::resolve( &ctx.registry @@ -94,9 +98,9 @@ impl ObjectType for QueryRoot { .get(&type_name) .map(|ty| __Type::new_simple(ctx.registry, ty)), &ctx_obj, + field.position, ) - .await - .map_err(|err| err.with_position(field.position).into()); + .await; } self.inner.resolve_field(ctx, field).await @@ -105,7 +109,11 @@ impl ObjectType for QueryRoot { #[async_trait::async_trait] impl OutputValueType for QueryRoot { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + async fn resolve( + value: &Self, + ctx: &ContextSelectionSet<'_>, + _pos: Pos, + ) -> Result { do_resolve(ctx, value).await } } diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 3ff52e00..6c8a5380 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -3,9 +3,8 @@ mod utils; mod visitor; mod visitors; -use crate::error::RuleErrors; use crate::registry::Registry; -use crate::{CacheControl, Result}; +use crate::{CacheControl, Error, Result}; use graphql_parser::query::Document; use visitor::{visit, VisitorContext, VisitorNil}; @@ -57,7 +56,7 @@ pub fn check_rules(registry: &Registry, doc: &Document) -> Result { visit(&mut visitor, &mut ctx, doc); if !ctx.errors.is_empty() { - return Err(RuleErrors { errors: ctx.errors }.into()); + return Err(Error::Rule { errors: ctx.errors }); } Ok(CheckResult { cache_control,