Reimplement the error type and remove the dependency on the anyhow::Error

This commit is contained in:
sunli 2020-04-02 10:21:04 +08:00
parent 1d6a892b94
commit 14860d9b88
32 changed files with 491 additions and 504 deletions

View File

@ -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<bool> {
async fn delete_book(&self, ctx: &Context<'_>, id: ID) -> FieldResult<bool> {
let mut books = ctx.data::<Storage>().lock().await;
let id = id.parse::<usize>()?;
if books.contains(id) {

View File

@ -112,7 +112,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result<TokenStre
#[#crate_name::async_trait::async_trait]
impl #crate_name::OutputValueType for #ident {
async fn resolve(value: &Self, _: &#crate_name::ContextSelectionSet<'_>) -> #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)
}
}

View File

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

View File

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

View File

@ -10,7 +10,9 @@ pub enum OutputType<'a> {
impl<'a> OutputType<'a> {
pub fn parse(input: &'a Type) -> Result<Self> {
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"));

View File

@ -76,8 +76,7 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result<T
resolvers.push(quote! {
if field.name.as_str() == #field_name {
let ctx_obj = ctx.with_selection_set(&field.selection_set);
return #crate_name::OutputValueType::resolve(&self.#ident, &ctx_obj).await.
map_err(|err| err.with_position(field.position).into());
return #crate_name::OutputValueType::resolve(&self.#ident, &ctx_obj, field.position).await;
}
});
@ -128,21 +127,18 @@ pub fn generate(object_args: &args::Object, input: &mut DeriveInput) -> Result<T
#[#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))
}
}
#[#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
}
}

View File

@ -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<std::any::TypeId, #crate_name::graphql_parser::query::Field>) -> #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(

View File

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

View File

@ -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<MyError> 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<String, MyError> {
Err(MyError::ServerError("The database is locked".to_owned()))
}
struct QueryRoot {}
struct QueryRoot;
#[Object]
impl QueryRoot {
#[field]
async fn do_not_find(&self) -> Result<i32> {
Err(MyError::NotFound)?
async fn do_not_find(&self) -> FieldResult<i32> {
Err(MyError::NotFound).extend_err(MyError::extend_err)
}
#[field]
async fn fail(&self) -> Result<String> {
Ok(get_my_error()?)
async fn fail(&self) -> FieldResult<String> {
Ok(get_my_error().extend_err(MyError::extend_err)?)
}
#[field]
async fn without_extensions(&self) -> Result<String> {
Err(MyError::ErrorWithoutExtensions)?
async fn without_extensions(&self) -> FieldResult<String> {
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<i32> {
async fn parse_value(&self, val: String) -> FieldResult<i32> {
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<i32> {
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(

View File

@ -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<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Human, EmptyEdgeFields>> {
) -> FieldResult<Connection<Human, EmptyEdgeFields>> {
let humans = ctx
.data::<StarWars>()
.humans()
@ -149,7 +149,7 @@ impl QueryRoot {
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Droid, EmptyEdgeFields>> {
) -> FieldResult<Connection<Droid, EmptyEdgeFields>> {
let droids = ctx
.data::<StarWars>()
.droids()

View File

@ -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<ID> {
fn from_global_id(id: ID) -> Option<ID> {
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<serde_json::Value>;
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value>;
}
#[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<BoxFieldFuture<'a>>,
) -> 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<serde_json::Value> {
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<serde_json::Value> {
value.to_json()
}
@ -229,7 +237,11 @@ impl<T: Type + Send + Sync> Type for &T {
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync> OutputValueType for &T {
#[allow(clippy::trivially_copy_pass_by_ref)]
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
T::resolve(*value, ctx).await
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value> {
T::resolve(*value, ctx, pos).await
}
}

View File

@ -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<serde_json::Value> = 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<QueryPathNode<'a>>,
#[allow(missing_docs)]
pub path_node: Option<QueryPathNode<'a>>,
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<Value> {
fn var_value(&self, name: &str, pos: Pos) -> Result<Value> {
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<Value> {
fn resolve_input_value(&self, mut value: Value, pos: Pos) -> Result<Value> {
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<T: InputValueType, F: FnOnce() -> Value>(
&self,
name: &str,
pos: Pos,
default: F,
) -> Result<T> {
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)
}

View File

@ -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<serde_json::Value>);
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<T> = std::result::Result<T, FieldError>;
impl<E> From<E> 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<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;
}
impl<T, E> ResultExt<T, E> for std::result::Result<T, E>
where
E: StdError + 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)))
}
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<serde_json::Value>,
},
}
/// Creates a wrapper with an error location
#[allow(missing_docs)]
pub trait ErrorWithPosition {
type Result;
fn with_position(self, position: Pos) -> PositionError;
}
impl<T: Into<Error>> 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<Pos>,
pub message: String,
}
#[derive(Debug, Error)]
pub struct RuleErrors {
pub errors: Vec<RuleError>,
}
impl Display for RuleErrors {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for error in &self.errors {
writeln!(f, "{}", error.message)?;
impl From<ParseError> 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<T, E>
where
Self: Sized,
E: std::fmt::Display + Sized,
{
fn extend_err<CB>(self, cb: CB) -> crate::Result<T>
where
CB: FnOnce(&E) -> serde_json::Value;
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Parse error: {message}")]
Parse { message: String },
impl<T, E> ResultExt<T, E> for std::result::Result<T, E>
where
E: std::fmt::Display + Sized,
{
fn extend_err<C>(self, cb: C) -> crate::Result<T>
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<serde_json::Value>,
err: QueryError,
},
#[error("Rule error")]
Rule { errors: Vec<RuleError> },
}

View File

@ -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<serde_json::Value> = 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(),

View File

@ -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<QueryResult>);
pub struct GQLResponse(pub Result<QueryResponse>);
impl Serialize for GQLResponse {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
@ -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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(err) = self.0.downcast_ref::<PositionError>() {
let mut seq = serializer.serialize_seq(Some(1))?;
seq.serialize_element(&GQLPositionError(err))?;
seq.end()
} else if let Some(err) = self.0.downcast_ref::<RuleErrors>() {
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::<Vec<_>>(),
}))?;
}
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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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::<Vec<_>>(),
)?;
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::<ExtendedError>() {
if extended_err.1.is_object() {
return Some(&extended_err.1);
}
}
None
}

View File

@ -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<T>`
pub type Result<T> = anyhow::Result<T>;
/// Error type, are actually `anyhow::Error`
pub type Error = anyhow::Error;
pub type Result<T> = std::result::Result<T, Error>;
// internal types
#[doc(hidden)]
@ -176,7 +172,7 @@ pub use types::{EnumItem, EnumType};
/// - Option<T>, such as `Option<i32>`
/// - Object and &Object
/// - Enum
/// - Result<T, E>, such as `Result<i32, E>`
/// - FieldResult<T, E>, such as `FieldResult<i32, E>`
///
/// # Context
///
@ -211,7 +207,7 @@ pub use types::{EnumItem, EnumType};
/// }
///
/// #[field(desc = "value with error")]
/// async fn value_with_error(&self) -> Result<i32> {
/// async fn value_with_error(&self) -> FieldResult<i32> {
/// Ok(self.value)
/// }
///

View File

@ -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<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
}
/// Execute the query.
pub async fn execute(self) -> Result<QueryResult>
pub async fn execute(self) -> Result<QueryResponse>
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<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
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(

View File

@ -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<BoxFieldFuture<'a>>,
) -> 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,
)?;

View File

@ -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<serde_json::Value> {
async fn resolve(
value: &Self,
_: &ContextSelectionSet<'_>,
_pos: Pos,
) -> Result<serde_json::Value> {
Ok((*value).into())
}
}

View File

@ -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::<Vec<_>>();
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::<Error>::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::<Error>::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<T: SubscriptionType>(
return Err(QueryError::UnknownFragment {
name: fragment_spread.fragment_name.clone(),
}
.into());
.into_error(fragment_spread.position));
}
}
Selection::InlineFragment(inline_fragment) => {

View File

@ -34,6 +34,9 @@ impl<Query, Mutation, Subscription> SubscriptionStubs<Query, Mutation, Subscript
///
/// You can customize your transport by implementing this trait.
pub trait SubscriptionTransport: Send + Sync + Unpin + 'static {
/// The error type.
type Error;
/// Parse the request data here.
/// If you have a new request, create a `SubscriptionStub` with the `Schema::create_subscription_stub`, and then call `SubscriptionStubs::add`.
/// You can return a `Byte`, which will be sent to the client. If it returns an error, the connection will be broken.
@ -42,7 +45,7 @@ pub trait SubscriptionTransport: Send + Sync + Unpin + 'static {
schema: &Schema<Query, Mutation, Subscription>,
stubs: &mut SubscriptionStubs<Query, Mutation, Subscription>,
data: Bytes,
) -> Result<Option<Bytes>>
) -> std::result::Result<Option<Bytes>, Self::Error>
where
Query: ObjectType + Sync + Send + 'static,
Mutation: ObjectType + Sync + Send + 'static,

View File

@ -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<Query, Mutation, Subscription>(
&mut self,
schema: &Schema<Query, Mutation, Subscription>,
stubs: &mut SubscriptionStubs<Query, Mutation, Subscription>,
data: Bytes,
) -> Result<Option<Bytes>>
) -> std::result::Result<Option<Bytes>, 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,
})))

View File

@ -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<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> 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<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
node,
})
.collect::<Vec<_>>();
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<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
.iter()
.map(|(_, _, item)| item)
.collect::<Vec<_>>();
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::<T, E>::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::<T, E>::type_name().to_string(),
},
})
}
}
@ -177,7 +174,11 @@ impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> OutputValueType
for Connection<T, E>
{
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
_pos: Pos,
) -> Result<serde_json::Value> {
do_resolve(ctx, value).await
}
}

View File

@ -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<serde_json::Value> {
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<serde_json::Value> {
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
_pos: Pos,
) -> Result<serde_json::Value> {
do_resolve(ctx, value).await
}
}

View File

@ -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<Connection<Self::Element, Self::EdgeFieldsObj>> {
/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
/// 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<String>,
/// first: Option<i32>,
/// last: Option<i32>
/// ) -> Result<Connection<i32, DiffFields>> {
/// ) -> FieldResult<Connection<i32, DiffFields>> {
/// 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<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Self::Element, Self::EdgeFieldsObj>> {
) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
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<Connection<Self::Element, Self::EdgeFieldsObj>>;
) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>>;
}

View File

@ -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<Connection<Self::Element, Self::EdgeFieldsObj>> {
) -> FieldResult<Connection<Self::Element, Self::EdgeFieldsObj>> {
let (start, end) = match operation {
QueryOperation::Forward { after, limit } => {
let start = after

View File

@ -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<serde_json::Value> {
Err(QueryError::NotConfiguredMutations.into())
async fn resolve(
_value: &Self,
_ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value> {
Err(Error::Query {
pos,
path: None,
err: QueryError::NotConfiguredMutations,
})
}
}

View File

@ -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<serde_json::Value> {
Err(QueryError::NotConfiguredSubscriptions.into())
async fn resolve(
_value: &Self,
_ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value> {
Err(Error::Query {
pos,
path: None,
err: QueryError::NotConfiguredSubscriptions,
})
}
}

View File

@ -1,4 +1,5 @@
use crate::{registry, ContextSelectionSet, InputValueType, OutputValueType, Result, Type, Value};
use graphql_parser::Pos;
use std::borrow::Cow;
impl<T: Type> Type for Vec<T> {
@ -34,11 +35,15 @@ impl<T: InputValueType> InputValueType for Vec<T> {
#[allow(clippy::ptr_arg)]
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync> OutputValueType for Vec<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value> {
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<T: Type> Type for &[T] {
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync> OutputValueType for &[T] {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value> {
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())
}

View File

@ -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<T: Type> Type for Option<T> {
@ -27,10 +29,13 @@ impl<T: InputValueType> InputValueType for Option<T> {
#[async_trait::async_trait]
impl<T: OutputValueType + Sync> OutputValueType for Option<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> where
{
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
pos: Pos,
) -> Result<serde_json::Value> where {
if let Some(inner) = value {
OutputValueType::resolve(inner, ctx).await
OutputValueType::resolve(inner, ctx, pos).await
} else {
Ok(serde_json::Value::Null)
}

View File

@ -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<T: ObjectType + Send + Sync> ObjectType for QueryRoot<T> {
async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result<serde_json::Value> {
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<T: ObjectType + Send + Sync> ObjectType for QueryRoot<T> {
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<T: ObjectType + Send + Sync> ObjectType for QueryRoot<T> {
.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<T: ObjectType + Send + Sync> ObjectType for QueryRoot<T> {
#[async_trait::async_trait]
impl<T: ObjectType + Send + Sync> OutputValueType for QueryRoot<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
async fn resolve(
value: &Self,
ctx: &ContextSelectionSet<'_>,
_pos: Pos,
) -> Result<serde_json::Value> {
do_resolve(ctx, value).await
}
}

View File

@ -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<CheckResult> {
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,