Merge branch 'actix-web-v4-beta' into upgrade-actix-web

This commit is contained in:
Sunli 2021-11-07 21:21:39 +08:00 committed by GitHub
commit cc38b56e80
102 changed files with 2079 additions and 220 deletions

View File

@ -16,8 +16,8 @@ jobs:
fail-fast: false
matrix:
include:
- { rust: stable, os: ubuntu-latest }
- { rust: 1.54.0, os: ubuntu-latest }
# - { rust: stable, os: ubuntu-latest }
- { rust: 1.56.1, os: ubuntu-latest }
steps:
- name: Checkout
uses: actions/checkout@v2

View File

@ -10,6 +10,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: 1.56.1
override: true
- name: Run cargo-tarpaulin
uses: actions-rs/tarpaulin@v0.1
with:

View File

@ -1,2 +1,2 @@
edition = "2018"
edition = "2021"
newline_style = "unix"

View File

@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
- Add `chrono::Duration` custom scalar. [#689](https://github.com/async-graphql/async-graphql/pull/689)
- Implement `From<Option<Option<T>>>` for `MaybeUndefined<T>`.
- Add `MaybeUndefined::as_opt_ref`, `MaybeUndefined::as_opt_deref`, `MaybeUndefined::map`, `MaybeUndefined::map_value`, `MaybeUndefined::contains`, `MaybeUndefined::contains_value`, and `MaybeUndefined::transpose` methods.
- Made `MaybeUndefined::is_undefined`, `MaybeUndefined::is_null`, `MaybeUndefined::is_value`, `MaybeUndefined::value` and `MaybeUndefined::as_opt_ref` const.
- Add `ResolverError` type. [#671](https://github.com/async-graphql/async-graphql/issues/671)
- [async-graphql-axum] Bump axum from `0.2.5` to `0.3`.
- [async-graphql-poem] Export the HTTP headers in the `Context`.
## [2.11.0] 2021-11-03
- Use Rust `2021` edition.
- Subscription typename - [GraphQL - October 2021] [#681](https://github.com/async-graphql/async-graphql/issues/681)
- Allow directive on variable definition - [GraphQL - October 2021] [#678](https://github.com/async-graphql/async-graphql/issues/678)
- Specified By - [GraphQL - October 2021] [#677](https://github.com/async-graphql/async-graphql/issues/677)
- Add `specified_by_url` for `Tz`, `DateTime<Tz>`, `Url`, `Uuid` and `Upload` scalars.
- Number value literal lookahead restrictions - [GraphQL - October 2021] [#685](https://github.com/async-graphql/async-graphql/issues/685)
## [2.10.8] 2021-10-26
- [async-graphql-poem] Bump poem to `1.0.13`.
## [2.10.6] 2021-10-26
- Add derived for object & simple object & complex object. [#667](https://github.com/async-graphql/async-graphql/pull/667) [#670](https://github.com/async-graphql/async-graphql/pull/670)
- Respect query object field order. [#612](https://github.com/async-graphql/async-graphql/issues/612)
## [2.10.5] 2021-10-22
- Bump poem from `0.6.6` to `1.0.7`.
## [2.10.4] 2021-10-22
- Implement `Default` for ID #659.

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
edition = "2021"
description = "A GraphQL server library implemented in Rust"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql/"
@ -22,11 +22,12 @@ dataloader = ["futures-timer", "futures-channel", "lru"]
tracing = ["tracinglib", "tracing-futures"]
decimal = ["rust_decimal"]
cbor = ["serde_cbor"]
chrono-duration = ["chrono", "iso8601-duration"]
[dependencies]
async-graphql-derive = { path = "derive", version = "=2.10.4" }
async-graphql-value = { path = "value", version = "=2.10.4" }
async-graphql-parser = { path = "parser", version = "=2.10.4" }
async-graphql-derive = { path = "derive", version = "=2.11.0" }
async-graphql-value = { path = "value", version = "=2.11.0" }
async-graphql-parser = { path = "parser", version = "=2.11.0" }
async-stream = "0.3.0"
async-trait = "0.1.48"
@ -50,6 +51,7 @@ mime = "0.3.15"
bson = { version = "2.0.0", optional = true, features = ["chrono-0_4"] }
chrono = { version = "0.4.19", optional = true }
chrono-tz = { version = "0.5.3", optional = true }
iso8601-duration = { version = "0.1.0", optional = true }
log = { version = "0.4.14", optional = true }
secrecy = { version = "0.7.0", optional = true }
tracinglib = { version = "0.1.25", optional = true, package = "tracing" }

View File

@ -34,7 +34,7 @@
* [Docs](https://docs.rs/async-graphql)
* [GitHub repository](https://github.com/async-graphql/async-graphql)
* [Cargo package](https://crates.io/crates/async-graphql)
* Minimum supported Rust version: 1.54.0 or later
* Minimum supported Rust version: 1.56.1 or later
## Safety

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-derive"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
edition = "2021"
description = "Macros for async-graphql"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql/"
@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"]
proc-macro = true
[dependencies]
async-graphql-parser = { path = "../parser", version = "=2.10.4" }
async-graphql-parser = { path = "../parser", version = "=2.11.0" }
proc-macro2 = "1.0.24"
syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] }
quote = "1.0.9"

View File

@ -6,7 +6,7 @@ use syn::{
Attribute, Generics, Ident, Lit, LitBool, LitStr, Meta, NestedMeta, Path, Type, Visibility,
};
#[derive(FromMeta)]
#[derive(FromMeta, Clone)]
#[darling(default)]
pub struct CacheControl {
public: bool,
@ -46,7 +46,7 @@ impl FromMeta for DefaultValue {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Visible {
None,
HiddenAlways,
@ -86,7 +86,7 @@ pub struct ConcreteType {
pub params: PathList,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum Deprecation {
NoDeprecated,
Deprecated { reason: Option<String> },
@ -115,7 +115,7 @@ impl FromMeta for Deprecation {
}
}
#[derive(FromField)]
#[derive(FromField, Clone)]
#[darling(attributes(graphql), forward_attrs(doc))]
pub struct SimpleObjectField {
pub ident: Option<Ident>,
@ -143,6 +143,8 @@ pub struct SimpleObjectField {
pub guard: Option<Meta>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default, multiple)]
pub derived: Vec<DerivedField>,
}
#[derive(FromDeriveInput)]
@ -243,6 +245,19 @@ pub struct ObjectField {
pub guard: Option<Meta>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
#[darling(default, multiple)]
pub derived: Vec<DerivedField>,
}
#[derive(FromMeta, Default, Clone)]
#[darling(default)]
/// Derivied fields arguments: are used to generate derivied fields.
pub struct DerivedField {
pub name: Option<Ident>,
pub into: Option<String>,
pub with: Option<Path>,
#[darling(default)]
pub owned: Option<bool>,
}
#[derive(FromDeriveInput)]
@ -431,6 +446,7 @@ pub struct Scalar {
pub name: Option<String>,
pub use_type_description: bool,
pub visible: Option<Visible>,
pub specified_by_url: Option<String>,
}
#[derive(FromMeta, Default)]
@ -637,6 +653,8 @@ pub struct NewType {
pub name: NewTypeName,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default)]
pub specified_by_url: Option<String>,
}
#[derive(FromMeta, Default)]

View File

@ -1,7 +1,13 @@
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use std::iter::FromIterator;
use std::str::FromStr;
use syn::ext::IdentExt;
use syn::{Block, Error, ImplItem, ItemImpl, ReturnType};
use syn::{
punctuated::Punctuated, Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Token, Type,
TypeReference,
};
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
use crate::output_type::OutputType;
@ -23,6 +29,91 @@ pub fn generate(
let mut resolvers = Vec::new();
let mut schema_fields = Vec::new();
// Computation of the derivated fields
let mut derived_impls = vec![];
for item in &mut item_impl.items {
if let ImplItem::Method(method) = item {
let method_args: args::ObjectField =
parse_graphql_attrs(&method.attrs)?.unwrap_or_default();
for derived in method_args.derived {
if derived.name.is_some() && derived.into.is_some() {
let base_function_name = &method.sig.ident;
let name = derived.name.unwrap();
let with = derived.with;
let into = Type::Verbatim(
proc_macro2::TokenStream::from_str(&derived.into.unwrap()).unwrap(),
);
let mut new_impl = method.clone();
new_impl.sig.ident = name;
new_impl.sig.output =
syn::parse2::<ReturnType>(quote! { -> #crate_name::Result<#into> })
.expect("invalid result type");
let should_create_context = new_impl
.sig
.inputs
.iter()
.nth(1)
.map(|x| {
if let FnArg::Typed(pat) = x {
if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty {
if let Type::Path(path) = elem.as_ref() {
return path.path.segments.last().unwrap().ident
!= "Context";
}
}
};
true
})
.unwrap_or(true);
if should_create_context {
let arg_ctx = syn::parse2::<FnArg>(quote! { ctx: &Context<'_> })
.expect("invalid arg type");
new_impl.sig.inputs.insert(1, arg_ctx);
}
let other_atts: Punctuated<Ident, Token![,]> = Punctuated::from_iter(
new_impl
.sig
.inputs
.iter()
.filter_map(|x| match x {
FnArg::Typed(pat) => match &*pat.pat {
Pat::Ident(ident) => Some(Ok(ident.ident.clone())),
_ => Some(Err(Error::new_spanned(
&pat,
"Must be a simple argument",
))),
},
FnArg::Receiver(_) => None,
})
.collect::<Result<Vec<Ident>, Error>>()?
.into_iter(),
);
let new_block = match with {
Some(with) => quote!({
::std::result::Result::Ok(#with(#self_ty::#base_function_name(&self, #other_atts).await?))
}),
None => quote!({
{
::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into())
}
}),
};
new_impl.block = syn::parse2::<Block>(new_block).expect("invalid block");
derived_impls.push(ImplItem::Method(new_impl));
}
}
}
}
item_impl.items.append(&mut derived_impls);
for item in &mut item_impl.items {
if let ImplItem::Method(method) = item {
let method_args: args::ObjectField =

View File

@ -71,13 +71,9 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult<TokenStream> {
}
let remote_conversion = if let Some(remote) = &enum_args.remote {
let remote_ty = if let Ok(ty) = syn::parse_str::<syn::Type>(remote) {
ty
} else {
return Err(
Error::new_spanned(remote, format!("Invalid remote type: '{}'", remote)).into(),
);
};
let remote_ty = syn::parse_str::<syn::Type>(remote).map_err(|_| {
Error::new_spanned(remote, format!("Invalid remote type: '{}'", remote))
})?;
let local_to_remote_items = enum_items.iter().map(|item| {
quote! {

View File

@ -231,7 +231,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
}
fn to_value(&self) -> #crate_name::Value {
let mut map = ::std::collections::BTreeMap::new();
let mut map = #crate_name::indexmap::IndexMap::new();
#(#put_fields)*
#crate_name::Value::Object(map)
}
@ -272,7 +272,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
}
fn __internal_to_value(&self) -> #crate_name::Value where Self: #crate_name::InputType {
let mut map = ::std::collections::BTreeMap::new();
let mut map = #crate_name::indexmap::IndexMap::new();
#(#put_fields)*
#crate_name::Value::Object(map)
}

View File

@ -90,6 +90,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult<TokenStream
extends: #extends,
keys: ::std::option::Option::None,
visible: #visible,
is_subscription: false,
}
})
}

View File

@ -72,6 +72,7 @@ pub fn generate(object_args: &args::MergedSubscription) -> GeneratorResult<Token
extends: #extends,
keys: ::std::option::Option::None,
visible: #visible,
is_subscription: true,
}
})
}

View File

@ -38,12 +38,18 @@ pub fn generate(newtype_args: &args::NewType) -> GeneratorResult<TokenStream> {
None => quote! { <#inner_ty as #crate_name::Type>::type_name() },
};
let create_type_info = if let Some(name) = &gql_typename {
let specified_by_url = match &newtype_args.specified_by_url {
Some(specified_by_url) => quote! { ::std::option::Option::Some(#specified_by_url) },
None => quote! { ::std::option::Option::None },
};
quote! {
registry.create_type::<#ident, _>(|_| #crate_name::registry::MetaType::Scalar {
name: ::std::borrow::ToOwned::to_owned(#name),
description: #desc,
is_valid: |value| <#ident as #crate_name::ScalarType>::is_valid(value),
visible: #visible,
specified_by_url: #specified_by_url,
})
}
} else {

View File

@ -1,7 +1,13 @@
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use std::iter::FromIterator;
use std::str::FromStr;
use syn::ext::IdentExt;
use syn::{Block, Error, ImplItem, ItemImpl, ReturnType};
use syn::{
punctuated::Punctuated, Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Token, Type,
TypeReference,
};
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
use crate::output_type::OutputType;
@ -38,6 +44,91 @@ pub fn generate(
let mut add_keys = Vec::new();
let mut create_entity_types = Vec::new();
// Computation of the derivated fields
let mut derived_impls = vec![];
for item in &mut item_impl.items {
if let ImplItem::Method(method) = item {
let method_args: args::ObjectField =
parse_graphql_attrs(&method.attrs)?.unwrap_or_default();
for derived in method_args.derived {
if derived.name.is_some() && derived.into.is_some() {
let base_function_name = &method.sig.ident;
let name = derived.name.unwrap();
let with = derived.with;
let into = Type::Verbatim(
proc_macro2::TokenStream::from_str(&derived.into.unwrap()).unwrap(),
);
let mut new_impl = method.clone();
new_impl.sig.ident = name;
new_impl.sig.output =
syn::parse2::<ReturnType>(quote! { -> #crate_name::Result<#into> })
.expect("invalid result type");
let should_create_context = new_impl
.sig
.inputs
.iter()
.nth(1)
.map(|x| {
if let FnArg::Typed(pat) = x {
if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty {
if let Type::Path(path) = elem.as_ref() {
return path.path.segments.last().unwrap().ident
!= "Context";
}
}
};
true
})
.unwrap_or(true);
if should_create_context {
let arg_ctx = syn::parse2::<FnArg>(quote! { ctx: &Context<'_> })
.expect("invalid arg type");
new_impl.sig.inputs.insert(1, arg_ctx);
}
let other_atts: Punctuated<Ident, Token![,]> = Punctuated::from_iter(
new_impl
.sig
.inputs
.iter()
.filter_map(|x| match x {
FnArg::Typed(pat) => match &*pat.pat {
Pat::Ident(ident) => Some(Ok(ident.ident.clone())),
_ => Some(Err(Error::new_spanned(
&pat,
"Must be a simple argument",
))),
},
FnArg::Receiver(_) => None,
})
.collect::<Result<Vec<Ident>, Error>>()?
.into_iter(),
);
let new_block = match with {
Some(with) => quote!({
::std::result::Result::Ok(#with(#self_ty::#base_function_name(&self, #other_atts).await?))
}),
None => quote!({
{
::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into())
}
}),
};
new_impl.block = syn::parse2::<Block>(new_block).expect("invalid block");
derived_impls.push(ImplItem::Method(new_impl));
}
}
}
}
item_impl.items.append(&mut derived_impls);
for item in &mut item_impl.items {
if let ImplItem::Method(method) = item {
let method_args: args::ObjectField =
@ -472,6 +563,7 @@ pub fn generate(
extends: #extends,
keys: ::std::option::Option::None,
visible: #visible,
is_subscription: false,
});
#(#create_entity_types)*
#(#add_keys)*
@ -539,6 +631,7 @@ pub fn generate(
extends: #extends,
keys: ::std::option::Option::None,
visible: #visible,
is_subscription: false,
});
#(#create_entity_types)*
#(#add_keys)*

View File

@ -30,6 +30,11 @@ pub fn generate(
let generic = &item_impl.generics;
let where_clause = &item_impl.generics.where_clause;
let visible = visible_fn(&scalar_args.visible);
let specified_by_url = match &scalar_args.specified_by_url {
Some(specified_by_url) => quote! { ::std::option::Option::Some(#specified_by_url) },
None => quote! { ::std::option::Option::None },
};
let expanded = quote! {
#item_impl
@ -45,6 +50,7 @@ pub fn generate(
description: #desc,
is_valid: |value| <#self_ty as #crate_name::ScalarType>::is_valid(value),
visible: #visible,
specified_by_url: #specified_by_url,
})
}
}

View File

@ -1,14 +1,28 @@
use darling::ast::Data;
use proc_macro::TokenStream;
use quote::quote;
use std::str::FromStr;
use syn::ext::IdentExt;
use syn::Error;
use syn::{Error, Ident, Path, Type};
use crate::args::{self, RenameRuleExt, RenameTarget};
use crate::args::{self, RenameRuleExt, RenameTarget, SimpleObjectField};
use crate::utils::{
gen_deprecation, generate_guards, get_crate_name, get_rustdoc, visible_fn, GeneratorResult,
};
#[derive(Debug)]
struct DerivedFieldMetadata {
ident: Ident,
into: Type,
owned: Option<bool>,
with: Option<Path>,
}
struct SimpleObjectFieldGenerator<'a> {
field: &'a SimpleObjectField,
derived: Option<DerivedFieldMetadata>,
}
pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream> {
let crate_name = get_crate_name(object_args.internal);
let ident = &object_args.ident;
@ -37,15 +51,62 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
let mut resolvers = Vec::new();
let mut schema_fields = Vec::new();
let mut processed_fields: Vec<SimpleObjectFieldGenerator> = vec![];
// Before processing the fields, we generate the derivated fields
for field in &s.fields {
processed_fields.push(SimpleObjectFieldGenerator {
field,
derived: None,
});
for derived in &field.derived {
if derived.name.is_some() && derived.into.is_some() {
let name = derived.name.clone().unwrap();
let into = match syn::parse2::<Type>(
proc_macro2::TokenStream::from_str(&derived.into.clone().unwrap()).unwrap(),
) {
Ok(e) => e,
_ => {
return Err(Error::new_spanned(
&name,
"derived into must be a valid type.",
)
.into());
}
};
let derived = DerivedFieldMetadata {
ident: name,
into,
owned: derived.owned,
with: derived.with.clone(),
};
processed_fields.push(SimpleObjectFieldGenerator {
field,
derived: Some(derived),
})
}
}
}
for SimpleObjectFieldGenerator { field, derived } in &processed_fields {
if field.skip {
continue;
}
let ident = match &field.ident {
let base_ident = match &field.ident {
Some(ident) => ident,
None => return Err(Error::new_spanned(&ident, "All fields must be named.").into()),
};
let ident = if let Some(derived) = derived {
&derived.ident
} else {
base_ident
};
let field_name = field.name.clone().unwrap_or_else(|| {
object_args
.rename_fields
@ -65,7 +126,18 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
None => quote! { ::std::option::Option::None },
};
let vis = &field.vis;
let ty = &field.ty;
let ty = if let Some(derived) = derived {
&derived.into
} else {
&field.ty
};
let owned = if let Some(derived) = derived {
derived.owned.unwrap_or(field.owned)
} else {
field.owned
};
let cache_control = {
let public = field.cache_control.is_public();
@ -104,23 +176,41 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|guard| quote! { #guard.check(ctx).await.map_err(|err| err.into_server_error(ctx.item.pos))?; },
);
getters.push(if !field.owned {
let with_function = derived.as_ref().and_then(|x| x.with.as_ref());
let mut block = match !owned {
true => quote! {
&self.#base_ident
},
false => quote! {
::std::clone::Clone::clone(&self.#base_ident)
},
};
block = match (derived, with_function) {
(Some(_), Some(with)) => quote! {
#with(#block)
},
(Some(_), None) => quote! {
::std::convert::Into::into(#block)
},
(_, _) => block,
};
let ty = match !owned {
true => quote! { &#ty },
false => quote! { #ty },
};
getters.push(
quote! {
#[inline]
#[allow(missing_docs)]
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<&#ty> {
::std::result::Result::Ok(&self.#ident)
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<#ty> {
::std::result::Result::Ok(#block)
}
}
} else {
quote! {
#[inline]
#[allow(missing_docs)]
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<#ty> {
::std::result::Result::Ok(::std::clone::Clone::clone(&self.#ident))
}
}
});
);
resolvers.push(quote! {
if ctx.item.node.name.node == #field_name {
@ -203,6 +293,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
extends: #extends,
keys: ::std::option::Option::None,
visible: #visible,
is_subscription: false,
})
}
}
@ -253,6 +344,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
extends: #extends,
keys: ::std::option::Option::None,
visible: #visible,
is_subscription: false,
})
}

View File

@ -375,7 +375,7 @@ pub fn generate(
};
#crate_name::futures_util::pin_mut!(resolve_fut);
let mut resp = query_env.extensions.resolve(ri, &mut resolve_fut).await.map(|value| {
let mut map = ::std::collections::BTreeMap::new();
let mut map = #crate_name::indexmap::IndexMap::new();
map.insert(::std::clone::Clone::clone(&field_name), value.unwrap_or_default());
#crate_name::Response::new(#crate_name::Value::Object(map))
})
@ -456,6 +456,7 @@ pub fn generate(
extends: #extends,
keys: ::std::option::Option::None,
visible: ::std::option::Option::None,
is_subscription: true,
})
}
}

View File

@ -8,6 +8,8 @@
- [Context](context.md)
- [Error handling](error_handling.md)
- [Merging Objects / Subscriptions](merging_objects.md)
- [Derived fields](derived_fields.md)
- [Complex Object](define_complex_object.md)
- [Enum](define_enum.md)
- [Interface](define_interface.md)
- [Union](define_union.md)
@ -26,6 +28,9 @@
- [Apollo Tracing](apollo_tracing.md)
- [Query complexity and depth](depth_and_complexity.md)
- [Hide content in introspection](visibility.md)
- [Extensions](extensions.md)
- [How extensions are working](extensions_inner_working.md)
- [Available extensions](extensions_available.md)
- [Integrations](integrations.md)
- [Tide](integrations_to_tide.md)
- [Warp](integrations_to_warp.md)
@ -33,5 +38,4 @@
- [Advanced topics](advanced_topics.md)
- [Custom scalars](custom_scalars.md)
- [Optimizing N+1 queries](dataloader.md)
- [Custom extensions](custom_extensions.md)
- [Apollo Federation](apollo_federation.md)

View File

@ -1,7 +0,0 @@
# Custom extensions
A GraphQL extension object can receive events in various stages of a query's execution, and you can collect various kinds of data to be returned in the query results.
You can use `async_graphql::Extension` to define an extension object, and your application must call `Schema::extension` when your `Schema` is created.
You can refer to [Apollo Tracing](https://github.com/async-graphql/async-graphql/blob/master/src/extensions/tracing.rs) to implement your own extension types.

View File

@ -0,0 +1,103 @@
# Derived fields
When you are working on a GraphQL project, you usually have to explain and share how your scalars should
be interpreted by your consumers. Sometimes, you event want to have the same data and the same logic exposing
the data in another type.
Within `async-graphql` you can create derived fields for objects to generate derived fields.
Consider you want to create a `Date` scalar, to represent an event of time.
How will you represent and format this date? You could create a scalar `Date` where you specified it's the RFCXXX
implemented to format it.
With derived fields there is a simple way to support multiple representation of a `Date` easily:
```rust
struct DateRFC3339(chrono::DateTime);
struct DateRFC2822(chrono::DateTime);
#[Scalar]
impl ScalarType for DateRFC3339 {
fn parse(value: Value) -> InputValueResult { ... }
fn to_value(&self) -> Value {
Value::String(self.0.to_rfc3339())
}
}
#[Scalar]
impl ScalarType for DateRFC2822 {
fn parse(value: Value) -> InputValueResult { ... }
fn to_value(&self) -> Value {
Value::String(self.0.to_rfc2822())
}
}
impl From<DateRFC2822> for DateRFC3339 {
fn from(value: DateRFC2822) -> Self {
DateRFC3339(value.0)
}
}
struct Query;
#[Object]
impl Query {
#[graphql(derived(name = "date_rfc3339", into = "DateRFC3339"))]
async fn duration_rfc2822(&self, arg: String) -> DateRFC2822 {
todo!()
}
}
```
It will render a GraphQL like:
```graphql
type Query {
duration_rfc2822(arg: String): DateRFC2822!
duration_rfc3339(arg: String): DateRFC3339!
}
```
## Wrapper types
A derived field won't be able to manage everythings easily: without the specialization from the Rust language, you won't be able to implement specialized trait like:
```
impl From<Vec<U>> for Vec<T> {
...
}
```
So you wouldn't be able to generate derived fields for existing wrapper type structures like `Vec` or `Option`. But when you implement a `From<U> for T` you should be able to derived a `From<Vec<U>> for Vec<T>` and a `From<Option<U>> for Option<T>`.
We included a `with` parameter to help you define a function to call instead of using the `Into` trait implementation between wrapper structures.
### Example
```rust
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived(String);
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived2(String);
scalar!(ValueDerived);
scalar!(ValueDerived2);
impl From<ValueDerived> for ValueDerived2 {
fn from(value: ValueDerived) -> Self {
ValueDerived2(value.0)
}
}
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
value.map(|x| x.into())
}
#[derive(SimpleObject)]
struct TestObj {
#[graphql(derived(owned, name = "value2", into = "Option<ValueDerived2>", with = "option_to_option"))]
pub value1: Option<ValueDerived>,
}
```

View File

@ -0,0 +1,3 @@
# Extensions
`async-graphql` has the capability to be extended with extensions without having to modify the original source code. A lot of features can be added this way, and a lot of extensions already exists.

View File

@ -0,0 +1,54 @@
# Extensions available
There are a lot of available extensions in the `async-graphql` to empower your GraphQL Server, some of these documentations are documented here.
## Analyzer
*Available in the repository*
The `analyzer` extension will output a field containing `complexity` and `depth` in the response extension field of each query.
## Apollo Persisted Queries
*Available in the repository*
To improve network performance for large queries, you can enable this Persisted Queries extension. With this extension enabled, each unique query is associated to a unique identifier, so clients can send this identifier instead of the corresponding query string to reduce requests sizes.
This extension doesn't force you to use some cache strategy, you can choose the caching strategy you want, you'll just have to implement the `CacheStorage` trait:
```rust
#[async_trait::async_trait]
pub trait CacheStorage: Send + Sync + Clone + 'static {
/// Load the query by `key`.
async fn get(&self, key: String) -> Option<String>;
/// Save the query by `key`.
async fn set(&self, key: String, query: String);
}
```
### References
[Apollo doc - Persisted Queries](https://www.apollographql.com/docs/react/api/link/persisted-queries/)
## Apollo Tracing
*Available in the repository*
Apollo Tracing is an extension which includes analytics data for your queries. This extension works to follow the old and now deprecated [Apollo Tracing Spec](https://github.com/apollographql/apollo-tracing). If you want to check the newer Apollo Reporting Protocol, it's implemented by [async-graphql Apollo studio extension](https://github.com/async-graphql/async_graphql_apollo_studio_extension) for Apollo Studio.
## Apollo Studio
*Available at [async-graphql/async_graphql_apollo_studio_extension](https://github.com/async-graphql/async_graphql_apollo_studio_extension)*
Apollo Studio is a cloud platform that helps you build, validate, and secure your organization's graph (description from the official documentation). It's a service allowing you to monitor & work with your team around your GraphQL Schema. `async-graphql` provides an extension implementing the official [Apollo Specification](https://www.apollographql.com/docs/studio/setup-analytics/#third-party-support) available at [async-graphql-extension-apollo-tracing](https://github.com/async-graphql/async_graphql_apollo_studio_extension) and [Crates.io](https://crates.io/crates/async-graphql-extension-apollo-tracing).
## Logger
*Available in the repository*
Logger is a simple extension allowing you to add some logging feature to `async-graphql`. It's also a good example to learn how to create your own extension.
## OpenTelemetry
*Available in the repository*
OpenTelemetry is an extension providing an integration with the [opentelemetry crate](https://crates.io/crates/opentelemetry) to allow your application to capture distributed traces and metrics from `async-grraphql`.
## Tracing
*Available in the repository*
Tracing is a simple extension allowing you to add some tracing feature to `async-graphql`. A little like the `Logger` extension.

View File

@ -0,0 +1,162 @@
# How extensions are defined
An `async-graphql` extension is defined by implementing the trait `Extension` associated. The `Extension` trait allow you to insert custom code to some several steps used to respond to GraphQL's queries through `async-graphql`. With `Extensions`, your application can hook into the GraphQL's requests lifecycle to add behaviors about incoming requests or outgoing response.
`Extensions` are a lot like middleware from other frameworks, be careful when using those: when you use an extension **it'll be run for every GraphQL requests**.
Across every step, you'll have the `ExtensionContext` supplied with data about your current request execution. Feel free to check how it's constructed in the code, documentation about it will soon come.
## A word about middleware
For those who don't know, let's dig deeper into what is a middleware:
```rust
async fn middleware(&self, ctx: &ExtensionContext<'_>, next: NextMiddleware<'_>) -> MiddlewareResult {
// Logic to your middleware.
/*
* Final step to your middleware, we call the next function which will trigger
* the execution of the next middleware. It's like a `callback` in JavaScript.
*/
next.run(ctx).await
}
```
As you have seen, a `Middleware` is only a function calling the next function at the end, but we could also do a middleware with the `next` function at the start. This is where it's becoming tricky: depending on where you put your logics and where is the `next` call, your logic won't have the same execution order.
Depending on your logic code, you'll want to process it before or after the `next` call. If you need more information about middlewares, there are a lot of things in the web.
## Processing of a query
There are several steps to go to process a query to completion, you'll be able to create extension based on these hooks.
### request
First, when we receive a request, if it's not a subscription, the first function to be called will be `request`, it's the first step, it's the function called at the incoming request, and it's also the function which will output the response to the user.
Default implementation for `request`:
```rust
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
next.run(ctx).await
}
```
Depending on where you put your logic code, it'll be executed at the beginning or at the ending of the query being processed.
```rust
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
// The code here will be run before the prepare_request is executed.
let result = next.run(ctx).await;
// The code after the completion of this futue will be after the processing, just before sending the result to the user.
result
}
```
### prepare_request
Just after the `request`, we will have the `prepare_request` lifecycle, which will be hooked.
```rust
async fn prepare_request(
&self,
ctx: &ExtensionContext<'_>,
request: Request,
next: NextPrepareRequest<'_>,
) -> ServerResult<Request> {
// The code here will be un before the prepare_request is executed, just after the request lifecycle hook.
let result = next.run(ctx, request).await;
// The code here will be run just after the prepare_request
result
}
```
### parse_query
The `parse_query` will create a GraphQL `ExecutableDocument` on your query, it'll check if the query is valid for the GraphQL Spec. Usually the implemented spec in `async-graphql` tends to be the last stable one (October2021).
```rust
/// Called at parse query.
async fn parse_query(
&self,
ctx: &ExtensionContext<'_>,
// The raw query
query: &str,
// The variables
variables: &Variables,
next: NextParseQuery<'_>,
) -> ServerResult<ExecutableDocument> {
next.run(ctx, query, variables).await
}
```
### validation
The `validation` step will check (depending on your `validation_mode`) rules the query should abide to and give the client data about why the query is not valid.
```rust
/// Called at validation query.
async fn validation(
&self,
ctx: &ExtensionContext<'_>,
next: NextValidation<'_>,
) -> Result<ValidationResult, Vec<ServerError>> {
next.run(ctx).await
}
```
### execute
The `execution` step is a huge one, it'll start the execution of the query by calling each resolver concurrently for a `Query` and serially for a `Mutation`.
```rust
/// Called at execute query.
async fn execute(
&self,
ctx: &ExtensionContext<'_>,
operation_name: Option<&str>,
next: NextExecute<'_>,
) -> Response {
// Befoe starting resolving the whole query
let result = next.run(ctx, operation_name).await;
// After resolving the whole query
result
}
````
### resolve
The `resolve` step is launched for each field.
```rust
/// Called at resolve field.
async fn resolve(
&self,
ctx: &ExtensionContext<'_>,
info: ResolveInfo<'_>,
next: NextResolve<'_>,
) -> ServerResult<Option<Value>> {
// Logic before resolving the field
let result = next.run(ctx, info).await;
// Logic after resolving the field
result
}
```
### subscribe
The `subscribe` lifecycle has the same behavior as the `request` but for a `Subscritpion`.
```rust
/// Called at subscribe request.
fn subscribe<'s>(
&self,
ctx: &ExtensionContext<'_>,
stream: BoxStream<'s, Response>,
next: NextSubscribe<'_>,
) -> BoxStream<'s, Response> {
next.run(ctx, stream)
}
```

@ -1 +1 @@
Subproject commit df8cfdff35d25d1cef3c9fda4d98e639a55fffc2
Subproject commit 8641a72c8ccb78e924e2550cd1fbfa2e62319174

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-actix-web"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
edition = "2021"
description = "async-graphql for actix-web"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql-actix-web/"
@ -12,8 +12,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "=2.10.4" }
async-graphql = { path = "../..", version = "=2.11.0" }
actix = "0.12.0"
actix-http = "3.0.0-beta.11"
actix-web = { version = "4.0.0-beta.10", default-features = false }
@ -22,6 +21,7 @@ async-channel = "1.6.1"
futures-util = { version = "0.3.17", default-features = false }
serde_json = "1.0.64"
serde_urlencoded = "0.7.0"
futures-channel = "0.3.13"
[dev-dependencies]
actix-rt = "2.2.0"

View File

@ -174,4 +174,4 @@ impl Responder for Response {
}
res.body(serde_json::to_string(&self.0).unwrap())
}
}
}

View File

@ -212,4 +212,4 @@ where
.spawn(ctx)
}
}
}
}

View File

@ -241,4 +241,4 @@ async fn test_count() {
.into()
)
);
}
}

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-axum"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
edition = "2021"
description = "async-graphql for axum"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql-axum/"
@ -12,10 +12,10 @@ keywords = ["futures", "async", "graphql", "axum"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "=2.10.4" }
async-graphql = { path = "../..", version = "=2.11.0" }
async-trait = "0.1.51"
axum = { version = "0.2.5", features = ["ws", "headers"] }
axum = { version = "0.3", features = ["ws", "headers"] }
bytes = "1.0.1"
headers = "0.3.4"
http = "0.2.4"

View File

@ -1,5 +1,3 @@
use std::convert::TryFrom;
use axum::body::Body;
use axum::response::IntoResponse;
use headers::HeaderName;

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-poem"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
edition = "2021"
description = "async-graphql for poem"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql-poem/"
@ -11,11 +11,10 @@ repository = "https://github.com/async-graphql/async-graphql"
keywords = ["futures", "async", "graphql", "poem"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "=2.10.4" }
async-graphql = { path = "../..", version = "=2.11.0" }
poem = { version = "0.6.6", features = ["websocket"] }
poem = { version = "1.0.19", features = ["websocket"] }
futures-util = { version = "0.3.13", default-features = false }
serde_json = "1.0.66"
tokio-util = { version = "0.6.7", features = ["compat"] }

View File

@ -1,4 +1,5 @@
use async_graphql::http::MultipartOptions;
use poem::error::BadRequest;
use poem::http::{header, Method};
use poem::web::Query;
use poem::{async_trait, Error, FromRequest, Request, RequestBody, Result};
@ -12,7 +13,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt;
/// # Example
///
/// ```
/// use poem::{handler, RouteMethod, route, EndpointExt};
/// use poem::{handler, Route, post, EndpointExt};
/// use poem::web::{Json, Data};
/// use poem::middleware::AddData;
/// use async_graphql_poem::GraphQLRequest;
@ -35,7 +36,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt;
/// }
///
/// let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
/// let app = route().at("/", RouteMethod::new().post(index.with(AddData::new(schema))));
/// let app = Route::new().at("/", post(index.with(AddData::new(schema))));
/// ```
pub struct GraphQLRequest(pub async_graphql::Request);
@ -49,7 +50,7 @@ impl<'a> FromRequest<'a> for GraphQLRequest {
.await?
.0
.into_single()
.map_err(Error::bad_request)?,
.map_err(BadRequest)?,
))
}
}
@ -63,10 +64,7 @@ impl<'a> FromRequest<'a> for GraphQLBatchRequest {
async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
if req.method() == Method::GET {
let req = Query::from_request(req, body)
.await
.map_err(Error::bad_request)?
.0;
let req = Query::from_request(req, body).await?.0;
Ok(Self(async_graphql::BatchRequest::Single(req)))
} else {
let content_type = req
@ -80,8 +78,7 @@ impl<'a> FromRequest<'a> for GraphQLBatchRequest {
body.take()?.into_async_read().compat(),
MultipartOptions::default(),
)
.await
.map_err(Error::bad_request)?,
.await?,
))
}
}

View File

@ -4,8 +4,10 @@
mod extractor;
mod query;
mod response;
mod subscription;
pub use extractor::{GraphQLBatchRequest, GraphQLRequest};
pub use query::GraphQL;
pub use response::{GraphQLBatchResponse, GraphQLResponse};
pub use subscription::GraphQLSubscription;

View File

@ -1,15 +1,14 @@
use async_graphql::{BatchResponse as GraphQLBatchResponse, ObjectType, Schema, SubscriptionType};
use poem::web::Json;
use async_graphql::{ObjectType, Schema, SubscriptionType};
use poem::{async_trait, Endpoint, FromRequest, Request, Result};
use crate::GraphQLBatchRequest;
use crate::{GraphQLBatchRequest, GraphQLBatchResponse};
/// A GraphQL query endpoint.
///
/// # Example
///
/// ```
/// use poem::{route, RouteMethod};
/// use poem::{Route, post};
/// use async_graphql_poem::GraphQL;
/// use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
///
@ -25,7 +24,7 @@ use crate::GraphQLBatchRequest;
/// type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
///
/// let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
/// let app = route().at("/", RouteMethod::new().post(GraphQL::new(schema)));
/// let app = Route::new().at("/", post(GraphQL::new(schema)));
/// ```
pub struct GraphQL<Query, Mutation, Subscription> {
schema: Schema<Query, Mutation, Subscription>,
@ -45,11 +44,11 @@ where
Mutation: ObjectType + 'static,
Subscription: SubscriptionType + 'static,
{
type Output = Result<Json<GraphQLBatchResponse>>;
type Output = Result<GraphQLBatchResponse>;
async fn call(&self, req: Request) -> Self::Output {
let (req, mut body) = req.split();
let req = GraphQLBatchRequest::from_request(&req, &mut body).await?;
Ok(Json(self.schema.execute_batch(req.0).await))
Ok(GraphQLBatchResponse(self.schema.execute_batch(req.0).await))
}
}

View File

@ -0,0 +1,50 @@
use poem::http::header::HeaderName;
use poem::web::Json;
use poem::{IntoResponse, Response};
/// Response for `async_graphql::Request`.
pub struct GraphQLResponse(pub async_graphql::Response);
impl From<async_graphql::Response> for GraphQLResponse {
fn from(resp: async_graphql::Response) -> Self {
Self(resp)
}
}
impl IntoResponse for GraphQLResponse {
fn into_response(self) -> Response {
GraphQLBatchResponse(self.0.into()).into_response()
}
}
/// Response for `async_graphql::BatchRequest`.
pub struct GraphQLBatchResponse(pub async_graphql::BatchResponse);
impl From<async_graphql::BatchResponse> for GraphQLBatchResponse {
fn from(resp: async_graphql::BatchResponse) -> Self {
Self(resp)
}
}
impl IntoResponse for GraphQLBatchResponse {
fn into_response(self) -> Response {
let mut resp = Json(&self.0).into_response();
if self.0.is_ok() {
if let Some(cache_control) = self.0.cache_control().value() {
if let Ok(value) = cache_control.try_into() {
resp.headers_mut().insert("cache-control", value);
}
}
}
for (name, value) in self.0.http_headers() {
if let (Ok(name), Ok(value)) = (TryInto::<HeaderName>::try_into(name), value.try_into())
{
resp.headers_mut().append(name, value);
}
}
resp
}
}

View File

@ -12,7 +12,7 @@ use poem::{http, Endpoint, FromRequest, IntoResponse, Request, Response, Result}
/// # Example
///
/// ```
/// use poem::{route, RouteMethod};
/// use poem::{Route, get};
/// use async_graphql_poem::GraphQLSubscription;
/// use async_graphql::{EmptyMutation, Object, Schema, Subscription};
/// use futures_util::{Stream, stream};
@ -38,7 +38,7 @@ use poem::{http, Endpoint, FromRequest, IntoResponse, Request, Response, Result}
/// type MySchema = Schema<Query, EmptyMutation, Subscription>;
///
/// let schema = Schema::new(Query, EmptyMutation, Subscription);
/// let app = route().at("/ws", RouteMethod::new().get(GraphQLSubscription::new(schema)));
/// let app = Route::new().at("/ws", get(GraphQLSubscription::new(schema)));
/// ```
pub struct GraphQLSubscription<Query, Mutation, Subscription, F> {
schema: Schema<Query, Mutation, Subscription>,

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-rocket"
version = "2.10.4"
version = "2.11.0"
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
edition = "2018"
edition = "2021"
description = "async-graphql for Rocket.rs"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql/"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "=2.10.4" }
async-graphql = { path = "../..", version = "=2.11.0" }
rocket = { version = "0.5.0-rc.1", default-features = false }
serde = "1.0.126"

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-tide"
version = "2.10.4"
version = "2.11.0"
authors = ["vkill <vkill.net@gmail.com>"]
edition = "2018"
edition = "2021"
description = "async-graphql for tide"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql-tide/"
@ -16,7 +16,7 @@ default = ["websocket"]
websocket = ["tide-websockets"]
[dependencies]
async-graphql = { path = "../..", version = "=2.10.4" }
async-graphql = { path = "../..", version = "=2.11.0" }
async-trait = "0.1.48"
futures-util = "0.3.13"
serde_json = "1.0.64"

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-warp"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
edition = "2021"
description = "async-graphql for warp"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql-warp/"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql = { path = "../..", version = "=2.10.4" }
async-graphql = { path = "../..", version = "=2.11.0" }
warp = { version = "0.3.0", default-features = false, features = ["websocket"] }
futures-util = { version = "0.3.13", default-features = false, features = ["sink"] }

View File

@ -1,4 +1,3 @@
use std::convert::TryInto;
use std::io;
use std::io::ErrorKind;

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-parser"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
edition = "2021"
description = "GraphQL query parser for async-graphql"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql/"
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
categories = ["network-programming", "asynchronous"]
[dependencies]
async-graphql-value = { path = "../value", version = "=2.10.4" }
async-graphql-value = { path = "../value", version = "=2.11.0" }
pest = "2.1.3"
pest_derive = "2.1.0"
serde_json = "1.0.64"

View File

@ -10,7 +10,7 @@ executable_definition = { operation_definition | fragment_definition }
operation_definition = { named_operation_definition | selection_set }
named_operation_definition = { operation_type ~ name? ~ variable_definitions? ~ directives? ~ selection_set }
variable_definitions = { "(" ~ variable_definition* ~ ")" }
variable_definition = { variable ~ ":" ~ type_ ~ default_value? }
variable_definition = { variable ~ ":" ~ type_ ~ directives? ~ default_value? }
selection_set = { "{" ~ selection+ ~ "}" }
selection = { field | inline_fragment | fragment_spread }
@ -86,6 +86,7 @@ directive_location = {
| "FRAGMENT_DEFINITION"
| "FRAGMENT_SPREAD"
| "INLINE_FRAGMENT"
| "VARIABLE_DEFINITION"
| "SCHEMA"
| "SCALAR"
| "OBJECT"
@ -115,7 +116,7 @@ value = { variable | number | string | boolean | null | enum_value |
variable = { "$" ~ name }
number = @{ float | int }
number = @{ (float | int) ~ !name_start }
float = { int ~ ((fractional ~ exponent) | fractional | exponent) }
fractional = { "." ~ ASCII_DIGIT+ }
exponent = { ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ }
@ -164,4 +165,5 @@ arguments = { "(" ~ argument+ ~ ")" }
const_argument = { name ~ ":" ~ const_value }
argument = { name ~ ":" ~ value }
name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* }
name_start = @{ (ASCII_ALPHA | "_") }
name = @{ name_start ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* }

View File

@ -208,6 +208,8 @@ fn parse_variable_definition(
let variable = parse_variable(pairs.next().unwrap(), pc)?;
let var_type = parse_type(pairs.next().unwrap(), pc)?;
let directives = parse_opt_directives(&mut pairs, pc)?;
let default_value = parse_if_rule(&mut pairs, Rule::default_value, |pair| {
parse_default_value(pair, pc)
})?;
@ -218,6 +220,7 @@ fn parse_variable_definition(
VariableDefinition {
name: variable,
var_type,
directives,
default_value,
},
pos,

View File

@ -312,3 +312,21 @@ fn parse_name(pair: Pair<Rule>, pc: &mut PositionCalculator) -> Result<Positione
debug_assert_eq!(pair.as_rule(), Rule::name);
Ok(Positioned::new(Name::new(pair.as_str()), pc.step(&pair)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_number_lookahead_restrictions() {
GraphQLParser::parse(Rule::const_list, "[123 abc]").unwrap();
GraphQLParser::parse(Rule::const_list, "[123.0123 abc]").unwrap();
GraphQLParser::parse(Rule::const_list, "[123.0123e7 abc]").unwrap();
GraphQLParser::parse(Rule::const_list, "[123.0123e77 abc]").unwrap();
assert!(GraphQLParser::parse(Rule::const_list, "[123abc]").is_err());
assert!(GraphQLParser::parse(Rule::const_list, "[123.0123abc]").is_err());
assert!(GraphQLParser::parse(Rule::const_list, "[123.0123e7abc]").is_err());
assert!(GraphQLParser::parse(Rule::const_list, "[123.0123e77abc]").is_err());
}
}

View File

@ -323,6 +323,7 @@ fn parse_directive_definition(
"FRAGMENT_DEFINITION" => DirectiveLocation::FragmentDefinition,
"FRAGMENT_SPREAD" => DirectiveLocation::FragmentSpread,
"INLINE_FRAGMENT" => DirectiveLocation::InlineFragment,
"VARIABLE_DEFINITION" => DirectiveLocation::VariableDefinition,
"SCHEMA" => DirectiveLocation::Schema,
"SCALAR" => DirectiveLocation::Scalar,
"OBJECT" => DirectiveLocation::Object,

View File

@ -10,7 +10,7 @@ use std::str::Chars;
/// Original position of an element in source code.
///
/// You can serialize and deserialize it to the GraphQL `locations` format
/// ([reference](https://spec.graphql.org/June2018/#sec-Errors)).
/// ([reference](https://spec.graphql.org/October2021/#sec-Errors)).
#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Default, Hash, Serialize, Deserialize)]
pub struct Pos {
/// One-based line number.

View File

@ -5,7 +5,7 @@ use async_graphql_value::{ConstValue, Name, Value};
/// An executable GraphQL file or request string.
///
/// [Reference](https://spec.graphql.org/June2018/#ExecutableDocument).
/// [Reference](https://spec.graphql.org/October2021/#ExecutableDocument).
#[derive(Debug, Clone)]
pub struct ExecutableDocument {
/// The operations of the document.
@ -93,7 +93,7 @@ enum OperationsIterInner<'a> {
/// A GraphQL operation, such as `mutation($content:String!) { makePost(content: $content) { id } }`.
///
/// [Reference](https://spec.graphql.org/June2018/#OperationDefinition).
/// [Reference](https://spec.graphql.org/October2021/#OperationDefinition).
#[derive(Debug, Clone)]
pub struct OperationDefinition {
/// The type of operation.
@ -108,13 +108,15 @@ pub struct OperationDefinition {
/// A variable definition inside a list of variable definitions, for example `$name:String!`.
///
/// [Reference](https://spec.graphql.org/June2018/#VariableDefinition).
/// [Reference](https://spec.graphql.org/October2021/#VariableDefinition).
#[derive(Debug, Clone)]
pub struct VariableDefinition {
/// The name of the variable, without the preceding `$`.
pub name: Positioned<Name>,
/// The type of the variable.
pub var_type: Positioned<Type>,
/// The variable's directives.
pub directives: Vec<Positioned<Directive>>,
/// The optional default value of the variable.
pub default_value: Option<Positioned<ConstValue>>,
}
@ -139,7 +141,7 @@ impl VariableDefinition {
/// A set of fields to be selected, for example `{ name age }`.
///
/// [Reference](https://spec.graphql.org/June2018/#SelectionSet).
/// [Reference](https://spec.graphql.org/October2021/#SelectionSet).
#[derive(Debug, Default, Clone)]
pub struct SelectionSet {
/// The fields to be selected.
@ -148,7 +150,7 @@ pub struct SelectionSet {
/// A part of an object to be selected; a single field, a fragment spread or an inline fragment.
///
/// [Reference](https://spec.graphql.org/June2018/#Selection).
/// [Reference](https://spec.graphql.org/October2021/#Selection).
#[derive(Debug, Clone)]
pub enum Selection {
/// Select a single field, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`.
@ -182,7 +184,7 @@ impl Selection {
/// A field being selected on an object, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`.
///
/// [Reference](https://spec.graphql.org/June2018/#Field).
/// [Reference](https://spec.graphql.org/October2021/#Field).
#[derive(Debug, Clone)]
pub struct Field {
/// The optional field alias.
@ -217,7 +219,7 @@ impl Field {
/// A fragment selector, such as `... userFields`.
///
/// [Reference](https://spec.graphql.org/June2018/#FragmentSpread).
/// [Reference](https://spec.graphql.org/October2021/#FragmentSpread).
#[derive(Debug, Clone)]
pub struct FragmentSpread {
/// The name of the fragment being selected.
@ -228,7 +230,7 @@ pub struct FragmentSpread {
/// An inline fragment selector, such as `... on User { name }`.
///
/// [Reference](https://spec.graphql.org/June2018/#InlineFragment).
/// [Reference](https://spec.graphql.org/October2021/#InlineFragment).
#[derive(Debug, Clone)]
pub struct InlineFragment {
/// The type condition.
@ -241,7 +243,7 @@ pub struct InlineFragment {
/// The definition of a fragment, such as `fragment userFields on User { name age }`.
///
/// [Reference](https://spec.graphql.org/June2018/#FragmentDefinition).
/// [Reference](https://spec.graphql.org/October2021/#FragmentDefinition).
#[derive(Debug, Clone)]
pub struct FragmentDefinition {
/// The type this fragment operates on.
@ -254,7 +256,7 @@ pub struct FragmentDefinition {
/// A type a fragment can apply to (`on` followed by the type).
///
/// [Reference](https://spec.graphql.org/June2018/#TypeCondition).
/// [Reference](https://spec.graphql.org/October2021/#TypeCondition).
#[derive(Debug, Clone)]
pub struct TypeCondition {
/// The type this fragment applies to.

View File

@ -4,7 +4,7 @@
//! [`ServiceDocument`](struct.ServiceDocument.html), representing an executable GraphQL query and a
//! GraphQL service respectively.
//!
//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/June2018/).
//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/October2021/).
mod executable;
mod service;
@ -19,7 +19,7 @@ pub use service::*;
/// The type of an operation; `query`, `mutation` or `subscription`.
///
/// [Reference](https://spec.graphql.org/June2018/#OperationType).
/// [Reference](https://spec.graphql.org/October2021/#OperationType).
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum OperationType {
/// A query.
@ -42,7 +42,7 @@ impl Display for OperationType {
/// A GraphQL type, for example `String` or `[String!]!`.
///
/// [Reference](https://spec.graphql.org/June2018/#Type).
/// [Reference](https://spec.graphql.org/October2021/#Type).
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Type {
/// The base type.
@ -105,7 +105,7 @@ impl Display for BaseType {
/// from [`Directive`](struct.Directive.html) in that it uses [`ConstValue`](enum.ConstValue.html)
/// instead of [`Value`](enum.Value.html).
///
/// [Reference](https://spec.graphql.org/June2018/#Directive).
/// [Reference](https://spec.graphql.org/October2021/#Directive).
#[derive(Debug, Clone)]
pub struct ConstDirective {
/// The name of the directive.
@ -140,7 +140,7 @@ impl ConstDirective {
/// A GraphQL directive, such as `@deprecated(reason: "Use the other field")`.
///
/// [Reference](https://spec.graphql.org/June2018/#Directive).
/// [Reference](https://spec.graphql.org/October2021/#Directive).
#[derive(Debug, Clone)]
pub struct Directive {
/// The name of the directive.

View File

@ -5,7 +5,7 @@ use async_graphql_value::Name;
/// A GraphQL file or request string defining a GraphQL service.
///
/// [Reference](https://spec.graphql.org/June2018/#Document).
/// [Reference](https://spec.graphql.org/October2021/#Document).
#[derive(Debug, Clone)]
pub struct ServiceDocument {
/// The definitions of this document.
@ -14,8 +14,8 @@ pub struct ServiceDocument {
/// A definition concerning the type system of a GraphQL service.
///
/// [Reference](https://spec.graphql.org/June2018/#TypeSystemDefinition). This enum also covers
/// [extensions](https://spec.graphql.org/June2018/#TypeSystemExtension).
/// [Reference](https://spec.graphql.org/October2021/#TypeSystemDefinition). This enum also covers
/// [extensions](https://spec.graphql.org/October2021/#TypeSystemExtension).
#[derive(Debug, Clone)]
pub enum TypeSystemDefinition {
/// The definition of the schema of the service.
@ -28,8 +28,8 @@ pub enum TypeSystemDefinition {
/// The definition of the schema in a GraphQL service.
///
/// [Reference](https://spec.graphql.org/June2018/#SchemaDefinition). This also covers
/// [extensions](https://spec.graphql.org/June2018/#SchemaExtension).
/// [Reference](https://spec.graphql.org/October2021/#SchemaDefinition). This also covers
/// [extensions](https://spec.graphql.org/October2021/#SchemaExtension).
#[derive(Debug, Clone)]
pub struct SchemaDefinition {
/// Whether the schema is an extension of another schema.
@ -46,8 +46,8 @@ pub struct SchemaDefinition {
/// The definition of a type in a GraphQL service.
///
/// [Reference](https://spec.graphql.org/June2018/#TypeDefinition). This also covers
/// [extensions](https://spec.graphql.org/June2018/#TypeExtension).
/// [Reference](https://spec.graphql.org/October2021/#TypeDefinition). This also covers
/// [extensions](https://spec.graphql.org/October2021/#TypeExtension).
#[derive(Debug, Clone)]
pub struct TypeDefinition {
/// Whether the type is an extension of another type.
@ -81,7 +81,7 @@ pub enum TypeKind {
/// The definition of an object type.
///
/// [Reference](https://spec.graphql.org/June2018/#ObjectType).
/// [Reference](https://spec.graphql.org/October2021/#ObjectType).
#[derive(Debug, Clone)]
pub struct ObjectType {
/// The interfaces implemented by the object.
@ -92,7 +92,7 @@ pub struct ObjectType {
/// The definition of a field inside an object or interface.
///
/// [Reference](https://spec.graphql.org/June2018/#FieldDefinition).
/// [Reference](https://spec.graphql.org/October2021/#FieldDefinition).
#[derive(Debug, Clone)]
pub struct FieldDefinition {
/// The description of the field.
@ -109,7 +109,7 @@ pub struct FieldDefinition {
/// The definition of an interface type.
///
/// [Reference](https://spec.graphql.org/June2018/#InterfaceType).
/// [Reference](https://spec.graphql.org/October2021/#InterfaceType).
#[derive(Debug, Clone)]
pub struct InterfaceType {
/// The interfaces implemented by the interface.
@ -120,7 +120,7 @@ pub struct InterfaceType {
/// The definition of a union type.
///
/// [Reference](https://spec.graphql.org/June2018/#UnionType).
/// [Reference](https://spec.graphql.org/October2021/#UnionType).
#[derive(Debug, Clone)]
pub struct UnionType {
/// The member types of the union.
@ -129,7 +129,7 @@ pub struct UnionType {
/// The definition of an enum.
///
/// [Reference](https://spec.graphql.org/June2018/#EnumType).
/// [Reference](https://spec.graphql.org/October2021/#EnumType).
#[derive(Debug, Clone)]
pub struct EnumType {
/// The possible values of the enum.
@ -138,7 +138,7 @@ pub struct EnumType {
/// The definition of a value inside an enum.
///
/// [Reference](https://spec.graphql.org/June2018/#EnumValueDefinition).
/// [Reference](https://spec.graphql.org/October2021/#EnumValueDefinition).
#[derive(Debug, Clone)]
pub struct EnumValueDefinition {
/// The description of the argument.
@ -151,7 +151,7 @@ pub struct EnumValueDefinition {
/// The definition of an input object.
///
/// [Reference](https://spec.graphql.org/June2018/#InputObjectType).
/// [Reference](https://spec.graphql.org/October2021/#InputObjectType).
#[derive(Debug, Clone)]
pub struct InputObjectType {
/// The fields of the input object.
@ -160,7 +160,7 @@ pub struct InputObjectType {
/// The definition of an input value inside the arguments of a field.
///
/// [Reference](https://spec.graphql.org/June2018/#InputValueDefinition).
/// [Reference](https://spec.graphql.org/October2021/#InputValueDefinition).
#[derive(Debug, Clone)]
pub struct InputValueDefinition {
/// The description of the argument.
@ -177,7 +177,7 @@ pub struct InputValueDefinition {
/// The definition of a directive in a service.
///
/// [Reference](https://spec.graphql.org/June2018/#DirectiveDefinition).
/// [Reference](https://spec.graphql.org/October2021/#DirectiveDefinition).
#[derive(Debug, Clone)]
pub struct DirectiveDefinition {
/// The description of the directive.
@ -192,7 +192,7 @@ pub struct DirectiveDefinition {
/// Where a directive can apply to.
///
/// [Reference](https://spec.graphql.org/June2018/#DirectiveLocation).
/// [Reference](https://spec.graphql.org/October2021/#DirectiveLocation).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectiveLocation {
/// A [query](enum.OperationType.html#variant.Query) [operation](struct.OperationDefinition.html).
@ -233,4 +233,6 @@ pub enum DirectiveLocation {
/// An [input value definition](struct.InputValueDefinition.html) on an input object but not a
/// field.
InputFieldDefinition,
/// An [variable definition](struct.VariableDefinition.html).
VariableDefinition,
}

View File

@ -0,0 +1,3 @@
query Foo($a: Int @directive = 10, $b: Int @directive) {
value
}

View File

@ -4,3 +4,5 @@ directive @test1(service: String!) on FIELD_DEFINITION
directive @test2(service: String!) on FIELD
directive @test3(service: String!) on ENUM_VALUE
directive @test4(service: String!) on ENUM
directive @test5(service: String!) on VARIABLE_DEFINITION

View File

@ -1,6 +1,9 @@
use std::any::Any;
use std::collections::BTreeMap;
use std::error::Error as StdError;
use std::fmt::{self, Debug, Display, Formatter};
use std::marker::PhantomData;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@ -20,10 +23,13 @@ impl ErrorExtensionValues {
}
/// An error in a GraphQL server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
pub struct ServerError {
/// An explanatory message of the error.
pub message: String,
/// The source of the error, comes from [`ResolverError<T>`].
#[serde(skip)]
pub source: Option<Arc<dyn Any + Send + Sync>>,
/// Where the error occurred.
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub locations: Vec<Pos>,
@ -39,17 +45,55 @@ fn error_extensions_is_empty(values: &Option<ErrorExtensionValues>) -> bool {
values.as_ref().map_or(true, |values| values.0.is_empty())
}
impl Debug for ServerError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("ServerError")
.field("message", &self.message)
.field("locations", &self.locations)
.field("path", &self.path)
.field("extensions", &self.extensions)
.finish()
}
}
impl PartialEq for ServerError {
fn eq(&self, other: &Self) -> bool {
self.message.eq(&other.message)
&& self.locations.eq(&other.locations)
&& self.path.eq(&other.path)
&& self.extensions.eq(&other.extensions)
}
}
impl ServerError {
/// Create a new server error with the message.
pub fn new(message: impl Into<String>, pos: Option<Pos>) -> Self {
Self {
message: message.into(),
source: None,
locations: pos.map(|pos| vec![pos]).unwrap_or_default(),
path: Vec::new(),
extensions: None,
}
}
/// Get the source of the error.
///
/// # Examples
///
/// ```rust
/// use std::string::FromUtf8Error;
/// use async_graphql::{ResolverError, Error, ServerError, Pos};
///
/// let bytes = vec![0, 159];
/// let err: Error = String::from_utf8(bytes).map_err(ResolverError::new).unwrap_err().into();
/// let server_err: ServerError = err.into_server_error(Pos { line: 1, column: 1 });
/// assert!(server_err.source::<FromUtf8Error>().is_some());
/// ```
pub fn source<T: Any + Send + Sync>(&self) -> Option<&T> {
self.source.as_ref().map(|err| err.downcast_ref()).flatten()
}
#[doc(hidden)]
pub fn with_path(self, path: Vec<PathSegment>) -> Self {
Self { path, ..self }
@ -72,6 +116,7 @@ impl From<parser::Error> for ServerError {
fn from(e: parser::Error) -> Self {
Self {
message: e.to_string(),
source: None,
locations: e.positions().collect(),
path: Vec::new(),
extensions: None,
@ -160,20 +205,39 @@ impl<T: InputType, E: Display> From<E> for InputValueError<T> {
pub type InputValueResult<T> = Result<T, InputValueError<T>>;
/// An error with a message and optional extensions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[derive(Clone, Serialize)]
pub struct Error {
/// The error message.
pub message: String,
/// The source of the error, comes from [`ResolverError<T>`].
#[serde(skip)]
pub source: Option<Arc<dyn Any + Send + Sync>>,
/// Extensions to the error.
#[serde(skip_serializing_if = "error_extensions_is_empty")]
pub extensions: Option<ErrorExtensionValues>,
}
impl Debug for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Error")
.field("message", &self.message)
.field("extensions", &self.extensions)
.finish()
}
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
self.message.eq(&other.message) && self.extensions.eq(&other.extensions)
}
}
impl Error {
/// Create an error from the given error message.
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
extensions: None,
}
}
@ -183,6 +247,7 @@ impl Error {
pub fn into_server_error(self, pos: Pos) -> ServerError {
ServerError {
message: self.message,
source: self.source,
locations: vec![pos],
path: Vec::new(),
extensions: self.extensions,
@ -190,10 +255,21 @@ impl Error {
}
}
impl<T: Display> From<T> for Error {
impl<T: Display + Send + Sync + 'static> From<T> for Error {
fn from(e: T) -> Self {
Self {
message: e.to_string(),
source: None,
extensions: None,
}
}
}
impl From<ResolverError> for Error {
fn from(e: ResolverError) -> Self {
Self {
message: e.message,
source: Some(e.error),
extensions: None,
}
}
@ -267,40 +343,58 @@ impl From<mime::FromStrError> for ParseRequestError {
/// An error which can be extended into a `Error`.
pub trait ErrorExtensions: Sized {
/// Convert the error to a `Error`.
fn extend(&self) -> Error;
fn extend(self) -> Error;
/// Add extensions to the error, using a callback to make the extensions.
fn extend_with<C>(self, cb: C) -> Error
where
C: FnOnce(&Self, &mut ErrorExtensionValues),
{
let message = self.extend().message;
let mut extensions = self.extend().extensions.unwrap_or_default();
cb(&self, &mut extensions);
let mut new_extensions = Default::default();
cb(&self, &mut new_extensions);
let Error {
message,
source,
extensions,
} = self.extend();
let mut extensions = extensions.unwrap_or_default();
extensions.0.extend(new_extensions.0);
Error {
message,
source,
extensions: Some(extensions),
}
}
}
impl ErrorExtensions for Error {
fn extend(&self) -> Error {
self.clone()
fn extend(self) -> Error {
self
}
}
// implementing for &E instead of E gives the user the possibility to implement for E which does
// not conflict with this implementation acting as a fallback.
impl<E: std::fmt::Display> ErrorExtensions for &E {
fn extend(&self) -> Error {
impl<E: Display> ErrorExtensions for &E {
fn extend(self) -> Error {
Error {
message: self.to_string(),
source: None,
extensions: None,
}
}
}
impl ErrorExtensions for ResolverError {
#[inline]
fn extend(self) -> Error {
Error::from(self)
}
}
/// Extend a `Result`'s error value with [`ErrorExtensions`](trait.ErrorExtensions.html).
pub trait ResultExt<T, E>: Sized {
/// Extend the error value of the result with the callback.
@ -335,3 +429,27 @@ where
}
}
}
/// A wrapper around a dynamic error type for resolver.
#[derive(Debug)]
pub struct ResolverError {
message: String,
error: Arc<dyn Any + Send + Sync>,
}
impl<T: StdError + Send + Sync + 'static> From<T> for ResolverError {
fn from(err: T) -> Self {
Self {
message: err.to_string(),
error: Arc::new(err),
}
}
}
impl ResolverError {
/// Create a new failure.
#[inline]
pub fn new<T: StdError + Send + Sync + 'static>(err: T) -> Self {
From::from(err)
}
}

View File

@ -624,7 +624,7 @@ impl<'a> GraphQLPlaygroundConfig<'a> {
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use indexmap::IndexMap;
#[test]
fn test_with_setting_can_use_any_json_value() {
@ -634,7 +634,7 @@ mod tests {
.with_setting("number", 10)
.with_setting("null", Value::Null)
.with_setting("array", Vec::from([1, 2, 3]))
.with_setting("object", BTreeMap::new());
.with_setting("object", IndexMap::new());
let json = serde_json::to_value(settings).unwrap();
let settings = json["settings"].as_object().unwrap();

View File

@ -34,7 +34,7 @@
//! * [Docs](https://docs.rs/async-graphql)
//! * [GitHub repository](https://github.com/async-graphql/async-graphql)
//! * [Cargo package](https://crates.io/crates/async-graphql)
//! * Minimum supported Rust version: 1.51 or later
//! * Minimum supported Rust version: 1.56.1 or later
//!
//! ## Features
//!
@ -210,7 +210,7 @@ pub use base::{
};
pub use error::{
Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult,
ParseRequestError, PathSegment, Result, ResultExt, ServerError, ServerResult,
ParseRequestError, PathSegment, ResolverError, Result, ResultExt, ServerError, ServerResult,
};
pub use look_ahead::Lookahead;
pub use registry::CacheControl;
@ -279,6 +279,7 @@ pub type FieldResult<T> = Result<T>;
/// | default | Use `Default::default` for default value | none | Y |
/// | default | Argument default value | literal | Y |
/// | default_with | Expression to generate default value | code string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
/// | validator | Input value validator | [`InputValueValidator`](validators/trait.InputValueValidator.html) | Y |
/// | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
/// | complexity | Custom field complexity. | string | Y |
@ -288,6 +289,14 @@ pub type FieldResult<T> = Result<T>;
/// | serial | Resolve each field sequentially. | bool | Y |
/// | key | Is entity key(for Federation) | bool | Y |
///
/// # Derived argument parameters
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
/// | name | Generated derived field name | string | N |
/// | into | Type to derived an into | string | Y |
/// | with | Function to apply to manage advanced use cases | string| Y |
///
/// # Valid field return types
///
/// - Scalar values, such as `i32` and `bool`. `usize`, `isize`, `u128` and `i128` are not
@ -443,6 +452,7 @@ pub use async_graphql_derive::Object;
/// | name | Field name | string | Y |
/// | deprecation | Field deprecated | bool | Y |
/// | deprecation | Field deprecation reason | string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
/// | owned | Field resolver return a ownedship value | bool | Y |
/// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y |
/// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y |
@ -452,6 +462,16 @@ pub use async_graphql_derive::Object;
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
///
/// # Derived argument parameters
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
/// | name | Generated derived field name | string | N |
/// | into | Type to derived an into | string | Y |
/// | owned | Field resolver return a ownedship value | bool | Y |
/// | with | Function to apply to manage advanced use cases | string| Y |
///
///
/// # Examples
///
/// ```rust
@ -498,6 +518,7 @@ pub use async_graphql_derive::SimpleObject;
/// | name | Field name | string | Y |
/// | deprecation | Field deprecated | bool | Y |
/// | deprecation | Field deprecation reason | string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
/// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y |
/// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y |
/// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y |
@ -507,6 +528,14 @@ pub use async_graphql_derive::SimpleObject;
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Derived argument parameters
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
/// | name | Generated derived field name | string | N |
/// | into | Type to derived an into | string | Y |
/// | with | Function to apply to manage advanced use cases | string| Y |
///
/// # Examples
///
/// ```rust
@ -958,6 +987,7 @@ pub use async_graphql_derive::Subscription;
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
/// | name | Scalar name | string | Y |
/// | specified_by_url | Provide a specification URL for this scalar type, it must link to a human-readable specification of the data format, serialization and coercion rules for this scalar. | string | Y |
///
pub use async_graphql_derive::Scalar;
@ -971,8 +1001,9 @@ pub use async_graphql_derive::Scalar;
/// |-------------|---------------------------|----------|----------|
/// | name | If this attribute is provided then define a new scalar, otherwise it is just a transparent proxy for the internal scalar. | string | Y |
/// | name | If this attribute is provided then define a new scalar, otherwise it is just a transparent proxy for the internal scalar. | bool | Y |
/// | visible(Only valid for new scalars.) | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible(Only valid for new scalars.) | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | visible(Only valid for new scalars) | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible(Only valid for new scalars) | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | specified_by_url(Only valid for new scalars) | Provide a specification URL for this scalar type, it must link to a human-readable specification of the data format, serialization and coercion rules for this scalar. | string | Y |
///
/// # Examples
///

View File

@ -1,5 +1,4 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use crate::parser::types::{Field, FragmentDefinition, Selection, SelectionSet};
use crate::Context;

View File

@ -218,4 +218,16 @@ impl<'a> __Type<'a> {
None
}
}
#[graphql(name = "specifiedByURL")]
async fn specified_by_url(&self) -> Option<&'a str> {
if let TypeDetail::Named(registry::MetaType::Scalar {
specified_by_url, ..
}) = &self.detail
{
*specified_by_url
} else {
None
}
}
}

View File

@ -189,6 +189,7 @@ pub enum MetaType {
description: Option<&'static str>,
is_valid: fn(value: &Value) -> bool,
visible: Option<MetaVisibleFn>,
specified_by_url: Option<&'static str>,
},
Object {
name: String,
@ -198,6 +199,7 @@ pub enum MetaType {
extends: bool,
keys: Option<Vec<String>>,
visible: Option<MetaVisibleFn>,
is_subscription: bool,
},
Interface {
name: String,
@ -374,6 +376,7 @@ impl Registry {
extends: false,
keys: None,
visible: None,
is_subscription: false,
},
);
let ty = f(self);
@ -507,6 +510,7 @@ impl Registry {
extends: false,
keys: None,
visible: None,
is_subscription: false,
},
);

View File

@ -1,6 +1,5 @@
use std::any::Any;
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fmt::{self, Debug, Formatter};
use serde::{Deserialize, Deserializer, Serialize};

View File

@ -1,7 +1,8 @@
use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use indexmap::IndexMap;
use crate::extensions::ResolveInfo;
use crate::parser::types::Selection;
use crate::registry::MetaType;
@ -74,7 +75,7 @@ pub async fn resolve_container_serial<'a, T: ContainerType + ?Sized>(
resolve_container_inner(ctx, root, false).await
}
fn insert_value(target: &mut BTreeMap<Name, Value>, name: Name, value: Value) {
fn insert_value(target: &mut IndexMap<Name, Value>, name: Name, value: Value) {
if let Some(prev_value) = target.get_mut(&name) {
if let Value::Object(target_map) = prev_value {
if let Value::Object(obj) = value {
@ -118,7 +119,7 @@ async fn resolve_container_inner<'a, T: ContainerType + ?Sized>(
results
};
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
for (name, value) in res {
insert_value(&mut map, name, value);
}

View File

@ -68,6 +68,9 @@ pub trait ScalarType: Sized + Send {
/// // Rename to `MV` and add description.
/// // scalar!(MyValue, "MV", "This is my value");
///
/// // Rename to `MV`, add description and specifiedByURL.
/// // scalar!(MyValue, "MV", "This is my value", "https://tools.ietf.org/html/rfc4122");
///
/// struct Query;
///
/// #[Object]
@ -92,23 +95,47 @@ pub trait ScalarType: Sized + Send {
/// ```
#[macro_export]
macro_rules! scalar {
($ty:ty, $name:literal, $desc:literal, $specified_by_url:literal) => {
$crate::scalar_internal!(
$ty,
$name,
::std::option::Option::Some($desc),
::std::option::Option::Some($specified_by_url)
);
};
($ty:ty, $name:literal, $desc:literal) => {
$crate::scalar_internal!($ty, $name, ::std::option::Option::Some($desc));
$crate::scalar_internal!(
$ty,
$name,
::std::option::Option::Some($desc),
::std::option::Option::None
);
};
($ty:ty, $name:literal) => {
$crate::scalar_internal!($ty, $name, ::std::option::Option::None);
$crate::scalar_internal!(
$ty,
$name,
::std::option::Option::None,
::std::option::Option::None
);
};
($ty:ty) => {
$crate::scalar_internal!($ty, ::std::stringify!($ty), ::std::option::Option::None);
$crate::scalar_internal!(
$ty,
::std::stringify!($ty),
::std::option::Option::None,
::std::option::Option::None
);
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! scalar_internal {
($ty:ty, $name:expr, $desc:expr) => {
($ty:ty, $name:expr, $desc:expr, $specified_by_url:expr) => {
impl $crate::Type for $ty {
fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> {
::std::borrow::Cow::Borrowed($name)
@ -122,6 +149,7 @@ macro_rules! scalar_internal {
description: $desc,
is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value),
visible: ::std::option::Option::None,
specified_by_url: $specified_by_url,
})
}
}

View File

@ -98,6 +98,7 @@ impl Response {
}
/// Response for batchable queries
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum BatchResponse {

View File

@ -190,6 +190,7 @@ where
extends: false,
keys: None,
visible: None,
is_subscription: false,
}
})
}

View File

@ -107,6 +107,7 @@ where
extends: false,
keys: None,
visible: None,
is_subscription: false,
}
})
}

View File

@ -45,6 +45,7 @@ impl Type for EmptyMutation {
extends: false,
keys: None,
visible: None,
is_subscription: false,
})
}
}

View File

@ -25,6 +25,7 @@ impl Type for EmptySubscription {
extends: false,
keys: None,
visible: None,
is_subscription: true,
})
}
}

View File

@ -2,7 +2,11 @@ use chrono_tz::Tz;
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
#[Scalar(internal, name = "TimeZone")]
#[Scalar(
internal,
name = "TimeZone",
specified_by_url = "http://www.iana.org/time-zones"
)]
impl ScalarType for Tz {
fn parse(value: Value) -> InputValueResult<Self> {
match value {

View File

@ -5,7 +5,11 @@ use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
/// Implement the DateTime<FixedOffset> scalar
///
/// The input/output is a string in RFC3339 format.
#[Scalar(internal, name = "DateTime")]
#[Scalar(
internal,
name = "DateTime",
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
)]
impl ScalarType for DateTime<FixedOffset> {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {
@ -22,7 +26,11 @@ impl ScalarType for DateTime<FixedOffset> {
/// Implement the DateTime<Local> scalar
///
/// The input/output is a string in RFC3339 format.
#[Scalar(internal, name = "DateTime")]
#[Scalar(
internal,
name = "DateTime",
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
)]
impl ScalarType for DateTime<Local> {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {
@ -39,7 +47,11 @@ impl ScalarType for DateTime<Local> {
/// Implement the DateTime<Utc> scalar
///
/// The input/output is a string in RFC3339 format.
#[Scalar(internal, name = "DateTime")]
#[Scalar(
internal,
name = "DateTime",
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
)]
impl ScalarType for DateTime<Utc> {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {

25
src/types/external/duration.rs vendored Normal file
View File

@ -0,0 +1,25 @@
use chrono::Duration;
use iso8601_duration as iso8601;
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
/// Implement the Duration scalar
///
/// The input/output is a string in ISO8601 format.
#[Scalar(
internal,
name = "Duration",
specified_by_url = "https://en.wikipedia.org/wiki/ISO_8601#Durations"
)]
impl ScalarType for Duration {
fn parse(value: Value) -> InputValueResult<Self> {
match &value {
Value::String(s) => Ok(Duration::from_std(iso8601::Duration::parse(s)?.to_std())?),
_ => Err(InputValueError::expected_type(value)),
}
}
fn to_value(&self) -> Value {
Value::String(self.to_string())
}
}

View File

@ -2,6 +2,8 @@ use std::collections::BTreeMap;
use std::fmt::Display;
use std::str::FromStr;
use indexmap::IndexMap;
use crate::{
InputType, InputValueError, InputValueResult, Name, OutputType, Scalar, ScalarType, Value,
};
@ -33,7 +35,7 @@ where
}
fn to_value(&self) -> Value {
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
for (name, value) in self {
map.insert(Name::new(name.to_string()), value.to_value());
}

View File

@ -1,8 +1,10 @@
use std::collections::{BTreeMap, HashMap};
use std::collections::HashMap;
use std::fmt::Display;
use std::hash::Hash;
use std::str::FromStr;
use indexmap::IndexMap;
use crate::{
InputType, InputValueError, InputValueResult, Name, OutputType, Scalar, ScalarType, Value,
};
@ -34,7 +36,7 @@ where
}
fn to_value(&self) -> Value {
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
for (name, value) in self {
map.insert(Name::new(name.to_string()), value.to_value());
}

View File

@ -1,5 +1,4 @@
use std::borrow::Cow;
use std::convert::TryInto;
use crate::parser::types::Field;
use crate::resolver_utils::resolve_list;

View File

@ -20,6 +20,8 @@ mod chrono_tz;
mod datetime;
#[cfg(feature = "decimal")]
mod decimal;
#[cfg(feature = "chrono-duration")]
mod duration;
#[cfg(feature = "chrono")]
mod naive_time;
#[cfg(feature = "secrecy")]

View File

@ -2,7 +2,7 @@ use url::Url;
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
#[Scalar(internal)]
#[Scalar(internal, specified_by_url = "http://url.spec.whatwg.org/")]
/// URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/)
impl ScalarType for Url {
fn parse(value: Value) -> InputValueResult<Self> {

View File

@ -2,7 +2,11 @@ use uuid::Uuid;
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
#[Scalar(internal, name = "UUID")]
#[Scalar(
internal,
name = "UUID",
specified_by_url = "http://tools.ietf.org/html/rfc4122"
)]
/// A UUID is a unique 128-bit number, stored as 16 octets. UUIDs are parsed as Strings
/// within GraphQL. UUIDs are used to assign unique identifiers to entities without requiring a central
/// allocating authority.

View File

@ -1,4 +1,3 @@
use std::convert::TryFrom;
use std::num::ParseIntError;
use std::ops::{Deref, DerefMut};

View File

@ -85,6 +85,7 @@ impl<T> Type for OutputJson<T> {
description: None,
is_valid: |_| true,
visible: None,
specified_by_url: None,
})
}
}

View File

@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::ops::Deref;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@ -6,7 +7,7 @@ use crate::{registry, InputType, InputValueError, InputValueResult, Type, Value}
/// Similar to `Option`, but it has three states, `undefined`, `null` and `x`.
///
/// **Reference:** <https://spec.graphql.org/June2018/#sec-Null-Value>
/// **Reference:** <https://spec.graphql.org/October2021/#sec-Null-Value>
///
/// # Examples
///
@ -61,34 +62,34 @@ impl<T> Default for MaybeUndefined<T> {
}
impl<T> MaybeUndefined<T> {
/// Returns true if the MaybeUndefined<T> is undefined.
/// Returns true if the `MaybeUndefined<T>` is undefined.
#[inline]
pub fn is_undefined(&self) -> bool {
pub const fn is_undefined(&self) -> bool {
matches!(self, MaybeUndefined::Undefined)
}
/// Returns true if the MaybeUndefined<T> is null.
/// Returns true if the `MaybeUndefined<T>` is null.
#[inline]
pub fn is_null(&self) -> bool {
pub const fn is_null(&self) -> bool {
matches!(self, MaybeUndefined::Null)
}
/// Returns true if the MaybeUndefined<T> is value.
/// Returns true if the `MaybeUndefined<T>` contains value.
#[inline]
pub fn is_value(&self) -> bool {
pub const fn is_value(&self) -> bool {
matches!(self, MaybeUndefined::Value(_))
}
/// Borrow the value, returns `None` if the value is `undefined` or `null`, otherwise returns `Some(T)`.
/// Borrow the value, returns `None` if the the `MaybeUndefined<T>` is `undefined` or `null`, otherwise returns `Some(T)`.
#[inline]
pub fn value(&self) -> Option<&T> {
pub const fn value(&self) -> Option<&T> {
match self {
MaybeUndefined::Value(value) => Some(value),
_ => None,
}
}
/// Convert MaybeUndefined<T> to Option<T>.
/// Converts the `MaybeUndefined<T>` to `Option<T>`.
#[inline]
pub fn take(self) -> Option<T> {
match self {
@ -96,6 +97,81 @@ impl<T> MaybeUndefined<T> {
_ => None,
}
}
/// Converts the `MaybeUndefined<T>` to `Option<Option<T>>`.
#[inline]
pub const fn as_opt_ref(&self) -> Option<Option<&T>> {
match self {
MaybeUndefined::Undefined => None,
MaybeUndefined::Null => Some(None),
MaybeUndefined::Value(value) => Some(Some(value)),
}
}
/// Converts the `MaybeUndefined<T>` to `Option<Option<&U>>`.
#[inline]
pub fn as_opt_deref<U>(&self) -> Option<Option<&U>>
where
U: ?Sized,
T: Deref<Target = U>,
{
match self {
MaybeUndefined::Undefined => None,
MaybeUndefined::Null => Some(None),
MaybeUndefined::Value(value) => Some(Some(value.deref())),
}
}
/// Returns `true` if the `MaybeUndefined<T>` contains the given value.
#[inline]
pub fn contains_value<U>(&self, x: &U) -> bool
where
U: PartialEq<T>,
{
match self {
MaybeUndefined::Value(y) => x == y,
_ => false,
}
}
/// Returns `true` if the `MaybeUndefined<T>` contains the given nullable value.
#[inline]
pub fn contains<U>(&self, x: &Option<U>) -> bool
where
U: PartialEq<T>,
{
match self {
MaybeUndefined::Value(y) => matches!(x, Some(v) if v == y),
MaybeUndefined::Null => matches!(x, None),
MaybeUndefined::Undefined => false,
}
}
/// Maps a `MaybeUndefined<T>` to `MaybeUndefined<U>` by applying a function to the contained nullable value
#[inline]
pub fn map<U, F: FnOnce(Option<T>) -> Option<U>>(self, f: F) -> MaybeUndefined<U> {
match self {
MaybeUndefined::Value(v) => match f(Some(v)) {
Some(v) => MaybeUndefined::Value(v),
None => MaybeUndefined::Null,
},
MaybeUndefined::Null => match f(None) {
Some(v) => MaybeUndefined::Value(v),
None => MaybeUndefined::Null,
},
MaybeUndefined::Undefined => MaybeUndefined::Undefined,
}
}
/// Maps a `MaybeUndefined<T>` to `MaybeUndefined<U>` by applying a function to the contained value
#[inline]
pub fn map_value<U, F: FnOnce(T) -> U>(self, f: F) -> MaybeUndefined<U> {
match self {
MaybeUndefined::Value(v) => MaybeUndefined::Value(f(v)),
MaybeUndefined::Null => MaybeUndefined::Null,
MaybeUndefined::Undefined => MaybeUndefined::Undefined,
}
}
}
impl<T: Type> Type for MaybeUndefined<T> {
@ -132,6 +208,24 @@ impl<T: InputType> InputType for MaybeUndefined<T> {
}
}
impl<T, E> MaybeUndefined<Result<T, E>> {
/// Transposes a `MaybeUndefined` of a [`Result`] into a [`Result`] of a `MaybeUndefined`.
///
/// [`MaybeUndefined::Undefined`] will be mapped to [`Ok`]`(`[`MaybeUndefined::Undefined`]`)`.
/// [`MaybeUndefined::Null`] will be mapped to [`Ok`]`(`[`MaybeUndefined::Null`]`)`.
/// [`MaybeUndefined::Value`]`(`[`Ok`]`(_))` and [`MaybeUndefined::Value`]`(`[`Err`]`(_))` will be mapped to
/// [`Ok`]`(`[`MaybeUndefined::Value`]`(_))` and [`Err`]`(_)`.
#[inline]
pub fn transpose(self) -> Result<MaybeUndefined<T>, E> {
match self {
MaybeUndefined::Undefined => Ok(MaybeUndefined::Undefined),
MaybeUndefined::Null => Ok(MaybeUndefined::Null),
MaybeUndefined::Value(Ok(v)) => Ok(MaybeUndefined::Value(v)),
MaybeUndefined::Value(Err(e)) => Err(e),
}
}
}
impl<T: Serialize> Serialize for MaybeUndefined<T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
@ -166,6 +260,16 @@ impl<T> From<MaybeUndefined<T>> for Option<Option<T>> {
}
}
impl<T> From<Option<Option<T>>> for MaybeUndefined<T> {
fn from(value: Option<Option<T>>) -> Self {
match value {
Some(Some(value)) => Self::Value(value),
Some(None) => Self::Null,
None => Self::Undefined,
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
@ -260,4 +364,117 @@ mod tests {
Some(Some(42))
);
}
#[test]
fn test_as_opt_ref() {
let mut value: MaybeUndefined<String>;
let mut r: Option<Option<&String>>;
value = MaybeUndefined::Undefined;
r = value.as_opt_ref();
assert_eq!(r, None);
value = MaybeUndefined::Null;
r = value.as_opt_ref();
assert_eq!(r, Some(None));
value = MaybeUndefined::Value("abc".to_string());
r = value.as_opt_ref();
assert_eq!(r, Some(Some(&"abc".to_string())));
}
#[test]
fn test_as_opt_deref() {
let mut value: MaybeUndefined<String>;
let mut r: Option<Option<&str>>;
value = MaybeUndefined::Undefined;
r = value.as_opt_deref();
assert_eq!(r, None);
value = MaybeUndefined::Null;
r = value.as_opt_deref();
assert_eq!(r, Some(None));
value = MaybeUndefined::Value("abc".to_string());
r = value.as_opt_deref();
assert_eq!(r, Some(Some("abc")));
}
#[test]
fn test_contains_value() {
let test = "abc";
let mut value: MaybeUndefined<String> = MaybeUndefined::Undefined;
assert!(!value.contains_value(&test));
value = MaybeUndefined::Null;
assert!(!value.contains_value(&test));
value = MaybeUndefined::Value("abc".to_string());
assert!(value.contains_value(&test));
}
#[test]
fn test_contains() {
let test = Some("abc");
let none: Option<&str> = None;
let mut value: MaybeUndefined<String> = MaybeUndefined::Undefined;
assert!(!value.contains(&test));
assert!(!value.contains(&none));
value = MaybeUndefined::Null;
assert!(!value.contains(&test));
assert!(value.contains(&none));
value = MaybeUndefined::Value("abc".to_string());
assert!(value.contains(&test));
assert!(!value.contains(&none));
}
#[test]
fn test_map_value() {
let mut value: MaybeUndefined<i32> = MaybeUndefined::Undefined;
assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Undefined);
value = MaybeUndefined::Null;
assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Null);
value = MaybeUndefined::Value(5);
assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Value(true));
}
#[test]
fn test_map() {
let mut value: MaybeUndefined<i32> = MaybeUndefined::Undefined;
assert_eq!(value.map(|v| Some(v.is_some())), MaybeUndefined::Undefined);
value = MaybeUndefined::Null;
assert_eq!(
value.map(|v| Some(v.is_some())),
MaybeUndefined::Value(false)
);
value = MaybeUndefined::Value(5);
assert_eq!(
value.map(|v| Some(v.is_some())),
MaybeUndefined::Value(true)
);
}
#[test]
fn test_transpose() {
let mut value: MaybeUndefined<Result<i32, &'static str>> = MaybeUndefined::Undefined;
assert_eq!(value.transpose(), Ok(MaybeUndefined::Undefined));
value = MaybeUndefined::Null;
assert_eq!(value.transpose(), Ok(MaybeUndefined::Null));
value = MaybeUndefined::Value(Ok(5));
assert_eq!(value.transpose(), Ok(MaybeUndefined::Value(5)));
value = MaybeUndefined::Value(Err("eror"));
assert_eq!(value.transpose(), Err("eror"));
}
}

View File

@ -50,6 +50,7 @@ impl<A: Type, B: Type> Type for MergedObject<A, B> {
extends: false,
keys: None,
visible: None,
is_subscription: false,
}
})
}

View File

@ -111,6 +111,7 @@ impl Type for Upload {
description: None,
is_valid: |value| matches!(value, Value::String(_)),
visible: None,
specified_by_url: Some("https://github.com/jaydenseric/graphql-multipart-request-spec"),
})
}
}

View File

@ -329,4 +329,9 @@ mod tests {
"#,
);
}
#[test]
fn typename_in_subscription_root() {
expect_fails_rule!(factory, "subscription { __typename }");
}
}

View File

@ -4,6 +4,7 @@
use once_cell::sync::Lazy;
use crate::futures_util::Stream;
use crate::parser::types::ExecutableDocument;
use crate::validation::visitor::{visit, RuleError, Visitor, VisitorContext};
use crate::*;
@ -345,8 +346,17 @@ impl MutationRoot {
}
}
static TEST_HARNESS: Lazy<Schema<QueryRoot, MutationRoot, EmptySubscription>> =
Lazy::new(|| Schema::new(QueryRoot, MutationRoot, EmptySubscription));
pub struct SubscriptionRoot;
#[Subscription(internal)]
impl SubscriptionRoot {
async fn values(&self) -> impl Stream<Item = i32> {
futures_util::stream::once(async move { 10 })
}
}
static TEST_HARNESS: Lazy<Schema<QueryRoot, MutationRoot, SubscriptionRoot>> =
Lazy::new(|| Schema::new(QueryRoot, MutationRoot, SubscriptionRoot));
pub(crate) fn validate<'a, V, F>(
doc: &'a ExecutableDocument,

View File

@ -611,6 +611,17 @@ fn visit_selection<'a, V: Visitor<'a>>(
visit_field(v, ctx, field);
},
);
} else if ctx.current_type().map(|ty| match ty {
MetaType::Object {
is_subscription, ..
} => *is_subscription,
_ => false,
}) == Some(true)
{
ctx.report_error(
vec![field.pos],
"Unknown field \"__typename\" on type \"Subscription\".",
);
}
}
Selection::FragmentSpread(fragment_spread) => {
@ -840,6 +851,7 @@ impl From<RuleError> for ServerError {
fn from(e: RuleError) -> Self {
Self {
message: e.message,
source: None,
locations: e.locations,
path: Vec::new(),
extensions: e.extensions,

382
tests/derived_field.rs Normal file
View File

@ -0,0 +1,382 @@
use async_graphql::*;
#[tokio::test]
pub async fn test_derived_field_object() {
use serde::{Deserialize, Serialize};
struct Query;
#[derive(Serialize, Deserialize)]
struct ValueDerived(String);
scalar!(ValueDerived);
impl From<i32> for ValueDerived {
fn from(value: i32) -> Self {
ValueDerived(format!("{}", value))
}
}
#[Object]
impl Query {
#[graphql(derived(name = "value2", into = "ValueDerived"))]
async fn value1(&self, #[graphql(default = 100)] input: i32) -> i32 {
input
}
}
let query = "{ value1 value2 }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"value1": 100,
"value2": "100",
})
);
let query = "{ value1(input: 1) value2(input: 2) }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"value1": 1,
"value2": "2",
})
);
}
#[tokio::test]
pub async fn test_derived_field_object_with() {
use serde::{Deserialize, Serialize};
struct Query;
#[derive(Serialize, Deserialize)]
struct ValueDerived(String);
scalar!(ValueDerived);
impl From<i32> for ValueDerived {
fn from(value: i32) -> Self {
ValueDerived(format!("{}", value))
}
}
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
value.map(|x| x.into())
}
#[Object]
impl Query {
#[graphql(derived(
name = "value2",
into = "Option<ValueDerived>",
with = "option_to_option"
))]
async fn value1(&self, #[graphql(default = 100)] input: i32) -> Option<i32> {
Some(input)
}
}
let query = "{ value1 value2 }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"value1": 100,
"value2": "100",
})
);
let query = "{ value1(input: 1) value2(input: 2) }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"value1": 1,
"value2": "2",
})
);
}
#[tokio::test]
pub async fn test_derived_field_simple_object() {
use serde::{Deserialize, Serialize};
struct Query;
#[derive(Serialize, Deserialize)]
struct ValueDerived(String);
scalar!(ValueDerived);
impl From<i32> for ValueDerived {
fn from(value: i32) -> Self {
ValueDerived(format!("{}", value))
}
}
#[derive(SimpleObject)]
struct TestObj {
#[graphql(owned, derived(name = "value2", into = "ValueDerived"))]
pub value1: i32,
}
#[Object]
impl Query {
async fn test(&self, #[graphql(default = 100)] input: i32) -> TestObj {
TestObj { value1: input }
}
}
let query = "{ test { value1 value2 } }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"test": {
"value1": 100,
"value2": "100",
}
})
);
let query = "{ test(input: 2) { value1 value2 }}";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
dbg!(schema.execute(query).await);
assert_eq!(
schema.execute(query).await.data,
value!({
"test": {
"value1": 2,
"value2": "2",
}
})
);
}
#[tokio::test]
pub async fn test_derived_field_simple_object_option() {
use serde::{Deserialize, Serialize};
struct Query;
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived(String);
#[derive(Serialize, Deserialize, Clone)]
struct ValueDerived2(String);
scalar!(ValueDerived);
scalar!(ValueDerived2);
impl From<ValueDerived> for ValueDerived2 {
fn from(value: ValueDerived) -> Self {
ValueDerived2(value.0)
}
}
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
value.map(|x| x.into())
}
fn vec_to_vec<T, U: From<T>>(value: Vec<T>) -> Vec<U> {
value.into_iter().map(|x| x.into()).collect()
}
fn vecopt_to_vecopt<T, U: From<T>>(value: Vec<Option<T>>) -> Vec<Option<U>> {
value.into_iter().map(|x| x.map(|opt| opt.into())).collect()
}
fn optvec_to_optvec<T, U: From<T>>(value: Option<Vec<T>>) -> Option<Vec<U>> {
value.map(|x| x.into_iter().map(|y| y.into()).collect())
}
#[derive(SimpleObject)]
struct TestObj {
#[graphql(derived(
owned,
name = "value2",
into = "Option<ValueDerived2>",
with = "option_to_option"
))]
pub value1: Option<ValueDerived>,
#[graphql(derived(
owned,
name = "value_vec_2",
into = "Vec<ValueDerived2>",
with = "vec_to_vec"
))]
pub value_vec_1: Vec<ValueDerived>,
#[graphql(derived(
owned,
name = "value_opt_vec_2",
into = "Option<Vec<ValueDerived2>>",
with = "optvec_to_optvec"
))]
pub value_opt_vec_1: Option<Vec<ValueDerived>>,
#[graphql(derived(
owned,
name = "value_vec_opt_2",
into = "Vec<Option<ValueDerived2>>",
with = "vecopt_to_vecopt"
))]
pub value_vec_opt_1: Vec<Option<ValueDerived>>,
}
#[Object]
impl Query {
async fn test(&self) -> TestObj {
TestObj {
value1: Some(ValueDerived("Test".to_string())),
value_vec_1: vec![ValueDerived("Test".to_string())],
value_opt_vec_1: Some(vec![ValueDerived("Test".to_string())]),
value_vec_opt_1: vec![Some(ValueDerived("Test".to_string()))],
}
}
}
let query = "{ test { value1 value2 valueVec1 valueVec2 valueOptVec1 valueOptVec2 } }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"test": {
"value1": "Test",
"value2": "Test",
"valueVec1": vec!["Test"],
"valueVec2": vec!["Test"],
"valueOptVec1": vec!["Test"],
"valueOptVec2": vec!["Test"],
}
})
);
}
#[tokio::test]
pub async fn test_derived_field_complex_object() {
use serde::{Deserialize, Serialize};
#[derive(SimpleObject)]
#[graphql(complex)]
struct MyObj {
a: i32,
#[graphql(owned, derived(name = "f", into = "ValueDerived"))]
b: i32,
}
#[derive(Serialize, Deserialize)]
struct ValueDerived(String);
scalar!(ValueDerived);
impl From<i32> for ValueDerived {
fn from(value: i32) -> Self {
ValueDerived(format!("{}", value))
}
}
#[ComplexObject]
impl MyObj {
async fn c(&self) -> i32 {
self.a + self.b
}
#[graphql(derived(name = "e", into = "ValueDerived"))]
async fn d(&self, v: i32) -> i32 {
self.a + self.b + v
}
}
struct Query;
#[Object]
impl Query {
async fn obj(&self) -> MyObj {
MyObj { a: 10, b: 20 }
}
}
let query = "{ obj { a b c d(v:100) e(v: 200) f } }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
dbg!(schema.execute(query).await);
assert_eq!(
schema.execute(query).await.data,
value!({
"obj": {
"a": 10,
"b": 20,
"c": 30,
"d": 130,
"e": "230",
"f": "20",
},
})
);
}
#[tokio::test]
pub async fn test_derived_field_complex_object_derived() {
use serde::{Deserialize, Serialize};
#[derive(SimpleObject)]
#[graphql(complex)]
struct MyObj {
a: i32,
#[graphql(owned, derived(name = "f", into = "ValueDerived"))]
b: i32,
}
#[derive(Serialize, Deserialize)]
struct ValueDerived(String);
scalar!(ValueDerived);
impl From<i32> for ValueDerived {
fn from(value: i32) -> Self {
ValueDerived(format!("{}", value))
}
}
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
value.map(|x| x.into())
}
#[ComplexObject]
impl MyObj {
async fn c(&self) -> i32 {
self.a + self.b
}
#[graphql(derived(name = "e", into = "Option<ValueDerived>", with = "option_to_option"))]
async fn d(&self, v: i32) -> Option<i32> {
Some(self.a + self.b + v)
}
}
struct Query;
#[Object]
impl Query {
async fn obj(&self) -> MyObj {
MyObj { a: 10, b: 20 }
}
}
let query = "{ obj { a b c d(v:100) e(v: 200) f } }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"obj": {
"a": 10,
"b": 20,
"c": 30,
"d": 130,
"e": "230",
"f": "20",
},
})
);
}

View File

@ -75,3 +75,112 @@ pub async fn test_error_extensions() {
})
);
}
#[tokio::test]
pub async fn test_failure() {
#[derive(thiserror::Error, Debug, PartialEq)]
enum MyError {
#[error("error1")]
Error1,
#[error("error2")]
Error2,
}
struct Query;
#[Object]
impl Query {
async fn failure(&self) -> Result<i32> {
Err(ResolverError::new(MyError::Error1).into())
}
async fn failure2(&self) -> Result<i32> {
Err(ResolverError::new(MyError::Error2))?;
Ok(1)
}
async fn failure3(&self) -> Result<i32> {
Err(ResolverError::new(MyError::Error1)
.extend_with(|_, values| values.set("a", 1))
.extend_with(|_, values| values.set("b", 2)))?;
Ok(1)
}
async fn failure4(&self) -> Result<i32> {
Err(ResolverError::new(MyError::Error2))
.extend_err(|_, values| values.set("a", 1))
.extend_err(|_, values| values.set("b", 2))?;
Ok(1)
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let err = schema
.execute("{ failure }")
.await
.into_result()
.unwrap_err()
.remove(0);
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error1);
let err = schema
.execute("{ failure2 }")
.await
.into_result()
.unwrap_err()
.remove(0);
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error2);
let err = schema
.execute("{ failure3 }")
.await
.into_result()
.unwrap_err()
.remove(0);
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error1);
assert_eq!(
err.extensions,
Some({
let mut values = ErrorExtensionValues::default();
values.set("a", 1);
values.set("b", 2);
values
})
);
let err = schema
.execute("{ failure4 }")
.await
.into_result()
.unwrap_err()
.remove(0);
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error2);
assert_eq!(
err.extensions,
Some({
let mut values = ErrorExtensionValues::default();
values.set("a", 1);
values.set("b", 2);
values
})
);
}
#[tokio::test]
pub async fn test_failure2() {
#[derive(thiserror::Error, Debug, PartialEq)]
enum MyError {
#[error("error1")]
Error1,
}
struct Query;
#[Object]
impl Query {
async fn failure(&self) -> Result<i32, ResolverError> {
Err(MyError::Error1)?
}
}
}

View File

@ -242,6 +242,7 @@ pub async fn test_find_entity_with_context() {
schema.execute(query).await.into_result().unwrap_err(),
vec![ServerError {
message: "Not found".to_string(),
source: None,
locations: vec![Pos {
line: 2,
column: 13

View File

@ -85,6 +85,7 @@ pub async fn test_field_features() {
vec![ServerError {
message: r#"Unknown field "valueAbc" on type "QueryRoot". Did you mean "value"?"#
.to_owned(),
source: None,
locations: vec![Pos { column: 3, line: 1 }],
path: Vec::new(),
extensions: None,
@ -113,6 +114,7 @@ pub async fn test_field_features() {
vec![ServerError {
message: r#"Unknown field "valueAbc" on type "MyObj". Did you mean "value"?"#
.to_owned(),
source: None,
locations: vec![Pos { column: 9, line: 1 }],
path: Vec::new(),
extensions: None,
@ -148,6 +150,7 @@ pub async fn test_field_features() {
.errors,
vec![ServerError {
message: r#"Unknown field "valuesAbc" on type "SubscriptionRoot". Did you mean "values", "valuesBson"?"#.to_owned(),
source: None,
locations: vec![Pos {
column: 16,
line: 1

View File

@ -99,6 +99,7 @@ pub async fn test_guard_simple_rule() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -127,6 +128,7 @@ pub async fn test_guard_simple_rule() {
.errors,
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 16
@ -176,6 +178,7 @@ pub async fn test_guard_and_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -195,6 +198,7 @@ pub async fn test_guard_and_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -214,6 +218,7 @@ pub async fn test_guard_and_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -283,6 +288,7 @@ pub async fn test_guard_or_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -332,6 +338,7 @@ pub async fn test_guard_chain_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -352,6 +359,7 @@ pub async fn test_guard_chain_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -372,6 +380,7 @@ pub async fn test_guard_chain_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -392,6 +401,7 @@ pub async fn test_guard_chain_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -483,6 +493,7 @@ pub async fn test_guard_race_operator() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
@ -538,6 +549,7 @@ pub async fn test_guard_use_params() {
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("get".to_owned())],
extensions: None,

View File

@ -67,6 +67,7 @@ pub async fn test_input_validator_string_min_length() {
.expect_err(&should_fail_msg),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -84,6 +85,7 @@ pub async fn test_input_validator_string_min_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -177,6 +179,7 @@ pub async fn test_input_validator_string_max_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -194,6 +197,7 @@ pub async fn test_input_validator_string_max_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -286,6 +290,7 @@ pub async fn test_input_validator_chars_min_length() {
.expect_err(&should_fail_msg),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -303,6 +308,7 @@ pub async fn test_input_validator_chars_min_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -397,6 +403,7 @@ pub async fn test_input_validator_chars_max_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -414,6 +421,7 @@ pub async fn test_input_validator_chars_max_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -534,6 +542,7 @@ pub async fn test_input_validator_string_email() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -552,6 +561,7 @@ pub async fn test_input_validator_string_email() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -682,6 +692,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg.clone(),
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -700,6 +711,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg.clone(),
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -717,6 +729,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -735,6 +748,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -792,6 +806,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -810,6 +825,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -851,6 +867,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -869,6 +886,7 @@ pub async fn test_input_validator_string_mac() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -929,6 +947,7 @@ pub async fn test_input_validator_int_range() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -946,6 +965,7 @@ pub async fn test_input_validator_int_range() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1034,6 +1054,7 @@ pub async fn test_input_validator_int_less_than() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1051,6 +1072,7 @@ pub async fn test_input_validator_int_less_than() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1141,6 +1163,7 @@ pub async fn test_input_validator_int_greater_than() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1158,6 +1181,7 @@ pub async fn test_input_validator_int_greater_than() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1241,6 +1265,7 @@ pub async fn test_input_validator_int_nonzero() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1258,6 +1283,7 @@ pub async fn test_input_validator_int_nonzero() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1345,6 +1371,7 @@ pub async fn test_input_validator_int_equal() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1362,6 +1389,7 @@ pub async fn test_input_validator_int_equal() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1461,6 +1489,7 @@ pub async fn test_input_validator_list_max_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1478,6 +1507,7 @@ pub async fn test_input_validator_list_max_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1577,6 +1607,7 @@ pub async fn test_input_validator_list_min_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1594,6 +1625,7 @@ pub async fn test_input_validator_list_min_length() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1701,6 +1733,7 @@ pub async fn test_input_validator_operator_or() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1718,6 +1751,7 @@ pub async fn test_input_validator_operator_or() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1818,6 +1852,7 @@ pub async fn test_input_validator_operator_and() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 17
@ -1835,6 +1870,7 @@ pub async fn test_input_validator_operator_and() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 14
@ -1938,6 +1974,7 @@ pub async fn test_input_validator_variable() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: field_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 37
@ -1955,6 +1992,7 @@ pub async fn test_input_validator_variable() {
.expect_err(&should_fail_msg[..]),
vec![ServerError {
message: object_error_msg,
source: None,
locations: vec!(Pos {
line: 1,
column: 34
@ -2041,6 +2079,7 @@ pub async fn test_custom_input_validator_with_extensions() {
.expect_err(should_fail_msg),
vec![ServerError {
message: field_error_msg.into(),
source: None,
locations: vec!(Pos {
line: 1,
column: 17

View File

@ -18,6 +18,7 @@ pub async fn test_input_value_custom_error() {
vec![ServerError {
message: "Failed to parse \"Int\": Only integers from -128 to 127 are accepted."
.to_owned(),
source: None,
locations: vec![Pos {
line: 1,
column: 14,

View File

@ -167,6 +167,7 @@ pub async fn test_array_type() {
vec![ServerError {
message: r#"Failed to parse "[Int!]": Expected input type "[Int; 6]", found [Int; 5]."#
.to_owned(),
source: None,
locations: vec![Pos {
line: 1,
column: 22,

60
tests/preserve_order.rs Normal file
View File

@ -0,0 +1,60 @@
use async_graphql::*;
#[tokio::test]
pub async fn test_preserve_order() {
#[derive(SimpleObject)]
struct Root {
a: i32,
b: i32,
c: i32,
}
let schema = Schema::new(Root { a: 1, b: 2, c: 3 }, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute("{ a c b }")
.await
.into_result()
.unwrap()
.data,
value!({
"a": 1, "c": 3, "b": 2
})
);
assert_eq!(
serde_json::to_string(
&schema
.execute("{ a c b }")
.await
.into_result()
.unwrap()
.data
)
.unwrap(),
r#"{"a":1,"c":3,"b":2}"#
);
assert_eq!(
schema
.execute("{ c b a }")
.await
.into_result()
.unwrap()
.data,
value!({
"c": 3, "b": 2, "a": 1
})
);
assert_eq!(
serde_json::to_string(
&schema
.execute("{ c b a }")
.await
.into_result()
.unwrap()
.data
)
.unwrap(),
r#"{"c":3,"b":2,"a":1}"#
);
}

View File

@ -34,12 +34,14 @@ pub async fn test_fieldresult() {
errors: vec![
ServerError {
message: "TestError".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("error1".to_owned())],
extensions: None,
},
ServerError {
message: "TestError".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 19,
@ -60,6 +62,7 @@ pub async fn test_fieldresult() {
.unwrap_err(),
vec![ServerError {
message: "TestError".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("optError".to_owned())],
extensions: None,
@ -74,6 +77,7 @@ pub async fn test_fieldresult() {
.unwrap_err(),
vec![ServerError {
message: "TestError".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![
PathSegment::Field("vecError".to_owned()),
@ -188,6 +192,7 @@ pub async fn test_error_propagation() {
cache_control: Default::default(),
errors: vec![ServerError {
message: "myerror".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 20,
@ -215,6 +220,7 @@ pub async fn test_error_propagation() {
cache_control: Default::default(),
errors: vec![ServerError {
message: "myerror".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 23,
@ -238,6 +244,7 @@ pub async fn test_error_propagation() {
cache_control: Default::default(),
errors: vec![ServerError {
message: "myerror".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 23,
@ -267,6 +274,7 @@ pub async fn test_error_propagation() {
cache_control: Default::default(),
errors: vec![ServerError {
message: "myerror".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 23,

View File

@ -11,7 +11,12 @@ mod test_mod {
#[tokio::test]
pub async fn test_scalar_macro() {
scalar!(test_mod::MyValue, "MV", "DESC");
scalar!(
test_mod::MyValue,
"MV",
"DESC",
"https://tools.ietf.org/html/rfc4122"
);
struct Query;
@ -26,7 +31,7 @@ pub async fn test_scalar_macro() {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema
.execute(r#"{ __type(name:"MV") { name description } }"#)
.execute(r#"{ __type(name:"MV") { name description specifiedByURL } }"#)
.await
.into_result()
.unwrap()
@ -35,6 +40,7 @@ pub async fn test_scalar_macro() {
"__type": {
"name": "MV",
"description": "DESC",
"specifiedByURL": "https://tools.ietf.org/html/rfc4122",
}
})
);

View File

@ -345,6 +345,7 @@ pub async fn test_subscription_error() {
stream.next().await,
Some(Err(vec![ServerError {
message: "TestError".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 25
@ -390,6 +391,7 @@ pub async fn test_subscription_fieldresult() {
cache_control: Default::default(),
errors: vec![ServerError {
message: "StreamErr".to_string(),
source: None,
locations: vec![Pos {
line: 1,
column: 16

View File

@ -1,8 +1,8 @@
[package]
name = "async-graphql-value"
version = "2.10.4"
version = "2.11.0"
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
edition = "2018"
edition = "2021"
description = "GraphQL value for async-graphql"
license = "MIT/Apache-2.0"
documentation = "https://docs.rs/async-graphql/"
@ -15,3 +15,4 @@ categories = ["network-programming", "asynchronous"]
serde_json = "1.0.64"
serde = { version = "1.0.125", features = ["derive"] }
bytes = { version = "1.0.1", features = ["serde"] }
indexmap = { version = "1.7.0", features = ["serde"] }

View File

@ -1,6 +1,7 @@
use std::collections::BTreeMap;
use std::{fmt, vec};
use indexmap::IndexMap;
use crate::{ConstValue, Name};
use serde::de::{
@ -78,7 +79,7 @@ where
}
fn visit_object<'de, V>(
object: BTreeMap<Name, ConstValue>,
object: IndexMap<Name, ConstValue>,
visitor: V,
) -> Result<V::Value, DeserializerError>
where
@ -365,13 +366,13 @@ impl<'de> SeqAccess<'de> for SeqDeserializer {
}
struct MapDeserializer {
iter: <BTreeMap<Name, ConstValue> as IntoIterator>::IntoIter,
iter: <IndexMap<Name, ConstValue> as IntoIterator>::IntoIter,
value: Option<ConstValue>,
}
impl MapDeserializer {
#[inline]
fn new(map: BTreeMap<Name, ConstValue>) -> Self {
fn new(map: IndexMap<Name, ConstValue>) -> Self {
MapDeserializer {
iter: map.into_iter(),
value: None,

View File

@ -10,17 +10,17 @@ mod value_serde;
mod variables;
use std::borrow::{Borrow, Cow};
use std::collections::BTreeMap;
use std::convert::{TryFrom, TryInto};
use std::fmt::{self, Display, Formatter, Write};
use std::iter::FromIterator;
use std::ops::Deref;
use std::sync::Arc;
use bytes::Bytes;
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub use deserializer::{from_value, DeserializerError};
#[doc(hidden)]
pub use indexmap;
pub use serde_json::Number;
pub use serializer::{to_value, SerializerError};
@ -139,7 +139,7 @@ pub enum ConstValue {
/// A list of values.
List(Vec<ConstValue>),
/// An object. This is a map of keys to values.
Object(BTreeMap<Name, ConstValue>),
Object(IndexMap<Name, ConstValue>),
}
impl PartialEq for ConstValue {
@ -254,8 +254,8 @@ impl<T: Into<ConstValue>> From<Vec<T>> for ConstValue {
}
}
impl From<BTreeMap<Name, ConstValue>> for ConstValue {
fn from(f: BTreeMap<Name, ConstValue>) -> Self {
impl From<IndexMap<Name, ConstValue>> for ConstValue {
fn from(f: IndexMap<Name, ConstValue>) -> Self {
ConstValue::Object(f)
}
}
@ -364,7 +364,7 @@ pub enum Value {
/// A list of values.
List(Vec<Value>),
/// An object. This is a map of keys to values.
Object(BTreeMap<Name, Value>),
Object(IndexMap<Name, Value>),
}
impl Value {

View File

@ -191,7 +191,7 @@ macro_rules! value_internal {
({ $($tt:tt)+ }) => {
$crate::ConstValue::Object({
let mut object = std::collections::BTreeMap::new();
let mut object = $crate::indexmap::IndexMap::new();
$crate::value_internal!(@object object () ($($tt)+) ($($tt)+));
object
})
@ -227,7 +227,7 @@ macro_rules! value_expect_expr_comma {
#[cfg(test)]
mod tests {
use crate::{ConstValue, Name};
use std::collections::BTreeMap;
use indexmap::IndexMap;
#[test]
fn test_macro() {
@ -254,7 +254,7 @@ mod tests {
)
);
assert_eq!(value!({"a": 10, "b": true}), {
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
map.insert(Name::new("a"), ConstValue::Number(10.into()));
map.insert(Name::new("b"), ConstValue::Boolean(true));
ConstValue::Object(map)

View File

@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use indexmap::IndexMap;
use serde::ser::{self, Impossible};
use serde::Serialize;
@ -180,7 +180,7 @@ impl ser::Serializer for Serializer {
T: ser::Serialize,
{
value.serialize(self).map(|v| {
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
map.insert(Name::new(variant), v);
ConstValue::Object(map)
})
@ -222,7 +222,7 @@ impl ser::Serializer for Serializer {
#[inline]
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Ok(SerializeMap {
map: BTreeMap::new(),
map: IndexMap::new(),
key: None,
})
}
@ -233,7 +233,7 @@ impl ser::Serializer for Serializer {
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Ok(SerializeStruct(BTreeMap::new()))
Ok(SerializeStruct(IndexMap::new()))
}
#[inline]
@ -244,7 +244,7 @@ impl ser::Serializer for Serializer {
variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Ok(SerializeStructVariant(Name::new(variant), BTreeMap::new()))
Ok(SerializeStructVariant(Name::new(variant), IndexMap::new()))
}
#[inline]
@ -337,14 +337,14 @@ impl ser::SerializeTupleVariant for SerializeTupleVariant {
#[inline]
fn end(self) -> Result<Self::Ok, Self::Error> {
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
map.insert(self.0, ConstValue::List(self.1));
Ok(ConstValue::Object(map))
}
}
struct SerializeMap {
map: BTreeMap<Name, ConstValue>,
map: IndexMap<Name, ConstValue>,
key: Option<Name>,
}
@ -378,7 +378,7 @@ impl ser::SerializeMap for SerializeMap {
}
}
struct SerializeStruct(BTreeMap<Name, ConstValue>);
struct SerializeStruct(IndexMap<Name, ConstValue>);
impl ser::SerializeStruct for SerializeStruct {
type Ok = ConstValue;
@ -405,7 +405,7 @@ impl ser::SerializeStruct for SerializeStruct {
}
}
struct SerializeStructVariant(Name, BTreeMap<Name, ConstValue>);
struct SerializeStructVariant(Name, IndexMap<Name, ConstValue>);
impl ser::SerializeStructVariant for SerializeStructVariant {
type Ok = ConstValue;
@ -428,7 +428,7 @@ impl ser::SerializeStructVariant for SerializeStructVariant {
#[inline]
fn end(self) -> Result<Self::Ok, Self::Error> {
let mut map = BTreeMap::new();
let mut map = IndexMap::new();
map.insert(self.0, ConstValue::Object(self.1));
Ok(ConstValue::Object(map))
}

Some files were not shown because too many files have changed in this diff Show More