diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b1e901..829dee98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ 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). +# [3.0.38] 2022-4-8 + +- Update Axum integration to Axum 0.5.1 [#883](https://github.com/async-graphql/async-graphql/pull/883) +- Support macro type in enum variant. [#884](https://github.com/async-graphql/async-graphql/pull/884) +- Introduce process_with for input object [#817](https://github.com/async-graphql/async-graphql/pull/817) +- Add `MaybeUndefined::update_to` method. [#881](https://github.com/async-graphql/async-graphql/issues/881) + +# [3.0.37] 2022-3-30 + +- Panics when the same Rust type has the same name. [#880](https://github.com/async-graphql/async-graphql/issues/880) + # [3.0.36] 2022-3-22 - Generate `@deprecated` to SDL. [#874](https://github.com/async-graphql/async-graphql/issues/874) diff --git a/Cargo.toml b/Cargo.toml index 2ad924f4..ace952fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "3.0.37" +version = "3.0.38" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "A GraphQL server library implemented in Rust" @@ -26,9 +26,9 @@ chrono-duration = ["chrono", "iso8601-duration"] password-strength-validator = ["zxcvbn"] [dependencies] -async-graphql-derive = { path = "derive", version = "3.0.36" } -async-graphql-value = { path = "value", version = "3.0.36" } -async-graphql-parser = { path = "parser", version = "3.0.36" } +async-graphql-derive = { path = "derive", version = "3.0.38" } +async-graphql-value = { path = "value", version = "3.0.38" } +async-graphql-parser = { path = "parser", version = "3.0.38" } async-stream = "0.3.0" async-trait = "0.1.48" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 9d1f8b07..ff22be95 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "3.0.36" +version = "3.0.38" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "Macros for async-graphql" @@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"] proc-macro = true [dependencies] -async-graphql-parser = { path = "../parser", version = "3.0.36" } +async-graphql-parser = { path = "../parser", version = "3.0.38" } proc-macro2 = "1.0.24" syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] } quote = "1.0.9" diff --git a/derive/src/args.rs b/derive/src/args.rs index eb0c7d85..a6d88db5 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -154,6 +154,8 @@ pub struct SimpleObjectField { pub visible: Option, #[darling(default, multiple)] pub derived: Vec, + #[darling(default)] + pub process_with: Option, // for InputObject #[darling(default)] pub default: Option, @@ -212,6 +214,8 @@ pub struct Argument { pub default: Option, pub default_with: Option, pub validator: Option, + #[darling(default)] + pub process_with: Option, pub key: bool, // for entity pub visible: Option, pub secret: bool, @@ -372,6 +376,8 @@ pub struct InputObjectField { pub skip: bool, #[darling(default)] pub skip_input: bool, + #[darling(default)] + pub process_with: Option, // for SimpleObject #[darling(default)] pub skip_output: bool, @@ -547,6 +553,8 @@ pub struct SubscriptionFieldArgument { pub default: Option, pub default_with: Option, pub validator: Option, + #[darling(default)] + pub process_with: Option, pub visible: Option, pub secret: bool, } diff --git a/derive/src/complex_object.rs b/derive/src/complex_object.rs index 33322525..51b980be 100644 --- a/derive/src/complex_object.rs +++ b/derive/src/complex_object.rs @@ -215,6 +215,17 @@ pub fn generate( }); use_params.push(quote! { #ident }); + let param_ident = &ident.ident; + let process_with = match argument.process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #param_ident); + } + } + None => Default::default(), + }; + let validators = argument .validator .clone() @@ -225,10 +236,15 @@ pub fn generate( quote!(#ty), Some(quote!(.map_err(|err| err.into_server_error(__pos)))), )?; + let mut non_mut_ident = ident.clone(); + non_mut_ident.mutability = None; get_params.push(quote! { - #[allow(non_snake_case, unused_variables)] - let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?; + #[allow(non_snake_case, unused_variables, unused_mut)] + let (__pos, mut #non_mut_ident) = ctx.oneof_param_value::<#ty>()?; + #process_with #validators + #[allow(non_snake_case, unused_variables)] + let #ident = #non_mut_ident; }); } else { for ( @@ -240,6 +256,7 @@ pub fn generate( default, default_with, validator, + process_with, visible, secret, .. @@ -289,6 +306,17 @@ pub fn generate( None => quote! { ::std::option::Option::None }, }; + let param_ident = &ident.ident; + let process_with = match process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #param_ident); + } + } + None => Default::default(), + }; + let validators = validator.clone().unwrap_or_default().create_validators( &crate_name, quote!(&#ident), @@ -296,10 +324,15 @@ pub fn generate( Some(quote!(.map_err(|err| err.into_server_error(__pos)))), )?; + let mut non_mut_ident = ident.clone(); + non_mut_ident.mutability = None; get_params.push(quote! { - #[allow(non_snake_case)] - let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; + #[allow(non_snake_case, unused_mut)] + let (__pos, mut #non_mut_ident) = ctx.param_value::<#ty>(#name, #default)?; + #process_with #validators + #[allow(non_snake_case)] + let #ident = #non_mut_ident; }); } } diff --git a/derive/src/enum.rs b/derive/src/enum.rs index 9efab5d4..ea22520f 100644 --- a/derive/src/enum.rs +++ b/derive/src/enum.rs @@ -130,7 +130,7 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { } fn __create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - registry.create_input_type::(|registry| { + registry.create_input_type::(#crate_name::registry::MetaTypeId::Enum, |registry| { #crate_name::registry::MetaType::Enum { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 6d545738..a02aecda 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -75,6 +75,16 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult federation_fields.push((ty, name.clone())); + let process_with = match field.process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #ident); + } + } + None => Default::default(), + }; + let validators = field .validator .clone() @@ -99,9 +109,11 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }); get_fields.push(quote! { - let #ident: #ty = #crate_name::InputType::parse( + #[allow(unused_mut)] + let mut #ident: #ty = #crate_name::InputType::parse( ::std::option::Option::Some(#crate_name::Value::Object(::std::clone::Clone::clone(&obj))) ).map_err(#crate_name::InputValueError::propagate)?; + #process_with #validators }); @@ -137,8 +149,12 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult let #ident: #ty = { match obj.get(#name) { ::std::option::Option::Some(value) => { - #crate_name::InputType::parse(::std::option::Option::Some(::std::clone::Clone::clone(&value))) - .map_err(#crate_name::InputValueError::propagate)? + #[allow(unused_mut)] + let mut #ident = #crate_name::InputType::parse(::std::option::Option::Some(::std::clone::Clone::clone(&value))) + .map_err(#crate_name::InputValueError::propagate)?; + #process_with + #ident + }, ::std::option::Option::None => #default, } @@ -147,9 +163,10 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }); } else { get_fields.push(quote! { - #[allow(non_snake_case)] - let #ident: #ty = #crate_name::InputType::parse(obj.get(#name).cloned()) + #[allow(non_snake_case, unused_mut)] + let mut #ident: #ty = #crate_name::InputType::parse(obj.get(#name).cloned()) .map_err(#crate_name::InputValueError::propagate)?; + #process_with #validators }); } @@ -213,7 +230,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - registry.create_input_type::(|registry| #crate_name::registry::MetaType::InputObject { + registry.create_input_type::(#crate_name::registry::MetaTypeId::InputObject, |registry| #crate_name::registry::MetaType::InputObject { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, input_fields: { @@ -260,7 +277,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult #[allow(clippy::all, clippy::pedantic)] impl #impl_generics #ident #ty_generics #where_clause { fn __internal_create_type_info(registry: &mut #crate_name::registry::Registry, name: &str) -> ::std::string::String where Self: #crate_name::InputType { - registry.create_input_type::(|registry| #crate_name::registry::MetaType::InputObject { + registry.create_input_type::(#crate_name::registry::MetaTypeId::InputObject, |registry| #crate_name::registry::MetaType::InputObject { name: ::std::borrow::ToOwned::to_owned(name), description: #desc, input_fields: { diff --git a/derive/src/interface.rs b/derive/src/interface.rs index a8b5d0f4..0bdda0e0 100644 --- a/derive/src/interface.rs +++ b/derive/src/interface.rs @@ -374,7 +374,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult ::std::string::String { - registry.create_output_type::(|registry| { + registry.create_output_type::(#crate_name::registry::MetaTypeId::Interface, |registry| { #(#registry_types)* #crate_name::registry::MetaType::Interface { diff --git a/derive/src/merged_object.rs b/derive/src/merged_object.rs index b234e06a..362f2a71 100644 --- a/derive/src/merged_object.rs +++ b/derive/src/merged_object.rs @@ -82,7 +82,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult ::std::string::String { - registry.create_output_type::(|registry| { + registry.create_output_type::(#crate_name::registry::MetaTypeId::Object, |registry| { let mut fields = ::std::default::Default::default(); let mut cache_control = ::std::default::Default::default(); diff --git a/derive/src/newtype.rs b/derive/src/newtype.rs index 2405f89f..7113c1b9 100644 --- a/derive/src/newtype.rs +++ b/derive/src/newtype.rs @@ -44,7 +44,7 @@ pub fn generate(newtype_args: &args::NewType) -> GeneratorResult { }; quote! { - registry.create_input_type::<#ident, _>(|_| #crate_name::registry::MetaType::Scalar { + registry.create_input_type::<#ident, _>(#crate_name::registry::MetaTypeId::Scalar, |_| #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), diff --git a/derive/src/object.rs b/derive/src/object.rs index 3716bc8a..5326f1d8 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -189,7 +189,7 @@ pub fn generate( if is_key { get_federation_key.push(quote! { if let Some(fields) = <#ty as #crate_name::InputType>::federation_fields() { - key_str.push(format!("{} {}", #name, fields)); + key_str.push(format!("{} {}", #name, fields)); } else { key_str.push(#name.to_string()); } @@ -360,6 +360,17 @@ pub fn generate( }); use_params.push(quote! { #ident }); + let param_ident = &ident.ident; + let process_with = match argument.process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #param_ident); + } + } + None => Default::default(), + }; + let validators = argument .validator .clone() @@ -370,10 +381,15 @@ pub fn generate( quote!(#ty), Some(quote!(.map_err(|err| err.into_server_error(__pos)))), )?; + let mut non_mut_ident = ident.clone(); + non_mut_ident.mutability = None; get_params.push(quote! { - #[allow(non_snake_case, unused_variables)] - let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?; + #[allow(non_snake_case, unused_variables, unused_mut)] + let (__pos, mut #non_mut_ident) = ctx.oneof_param_value::<#ty>()?; + #process_with #validators + #[allow(non_snake_case, unused_variables)] + let #ident = #non_mut_ident; }); } else { for ( @@ -384,6 +400,7 @@ pub fn generate( desc, default, default_with, + process_with, validator, visible, secret, @@ -434,6 +451,16 @@ pub fn generate( None => quote! { ::std::option::Option::None }, }; + let process_with = match process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #param_ident); + } + } + None => Default::default(), + }; + let validators = validator.clone().unwrap_or_default().create_validators( &crate_name, quote!(&#ident), @@ -441,10 +468,15 @@ pub fn generate( Some(quote!(.map_err(|err| err.into_server_error(__pos)))), )?; + let mut non_mut_ident = ident.clone(); + non_mut_ident.mutability = None; get_params.push(quote! { - #[allow(non_snake_case, unused_variables)] - let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; + #[allow(non_snake_case, unused_variables, unused_mut)] + let (__pos, mut #non_mut_ident) = ctx.param_value::<#ty>(#name, #default)?; + #process_with #validators + #[allow(non_snake_case, unused_variables)] + let #ident = #non_mut_ident; }); } } @@ -653,7 +685,7 @@ pub fn generate( } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - let ty = registry.create_output_type::(|registry| #crate_name::registry::MetaType::Object { + let ty = registry.create_output_type::(#crate_name::registry::MetaTypeId::Object, |registry| #crate_name::registry::MetaType::Object { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, fields: { @@ -692,7 +724,7 @@ pub fn generate( impl #impl_generics #self_ty #where_clause { fn __internal_create_type_info(registry: &mut #crate_name::registry::Registry, name: &str) -> ::std::string::String where Self: #crate_name::OutputType { - let ty = registry.create_output_type::(|registry| #crate_name::registry::MetaType::Object { + let ty = registry.create_output_type::(#crate_name::registry::MetaTypeId::Object, |registry| #crate_name::registry::MetaType::Object { name: ::std::borrow::ToOwned::to_owned(name), description: #desc, fields: { diff --git a/derive/src/oneof_object.rs b/derive/src/oneof_object.rs index 588997f9..330b6e7d 100644 --- a/derive/src/oneof_object.rs +++ b/derive/src/oneof_object.rs @@ -131,7 +131,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - registry.create_input_type::(|registry| #crate_name::registry::MetaType::InputObject { + registry.create_input_type::(#crate_name::registry::MetaTypeId::InputObject, |registry| #crate_name::registry::MetaType::InputObject { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, input_fields: { @@ -181,7 +181,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult #[allow(clippy::all, clippy::pedantic)] impl #impl_generics #ident #ty_generics #where_clause { fn __internal_create_type_info(registry: &mut #crate_name::registry::Registry, name: &str) -> ::std::string::String where Self: #crate_name::InputType { - registry.create_input_type::(|registry| #crate_name::registry::MetaType::InputObject { + registry.create_input_type::(#crate_name::registry::MetaTypeId::InputObject, |registry| #crate_name::registry::MetaType::InputObject { name: ::std::borrow::ToOwned::to_owned(name), description: #desc, input_fields: { diff --git a/derive/src/scalar.rs b/derive/src/scalar.rs index a7feebfd..a4838f98 100644 --- a/derive/src/scalar.rs +++ b/derive/src/scalar.rs @@ -47,7 +47,7 @@ pub fn generate( } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - registry.create_input_type::<#self_ty, _>(|_| #crate_name::registry::MetaType::Scalar { + registry.create_input_type::<#self_ty, _>(#crate_name::registry::MetaTypeId::Scalar, |_| #crate_name::registry::MetaType::Scalar { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, is_valid: |value| <#self_ty as #crate_name::ScalarType>::is_valid(value), @@ -77,7 +77,7 @@ pub fn generate( } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - registry.create_output_type::<#self_ty, _>(|_| #crate_name::registry::MetaType::Scalar { + registry.create_output_type::<#self_ty, _>(#crate_name::registry::MetaTypeId::Scalar, |_| #crate_name::registry::MetaType::Scalar { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, is_valid: |value| <#self_ty as #crate_name::ScalarType>::is_valid(value), diff --git a/derive/src/simple_object.rs b/derive/src/simple_object.rs index a84500f8..900c3a75 100644 --- a/derive/src/simple_object.rs +++ b/derive/src/simple_object.rs @@ -311,7 +311,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult ::std::string::String { - registry.create_output_type::(|registry| #crate_name::registry::MetaType::Object { + registry.create_output_type::(#crate_name::registry::MetaTypeId::Object, |registry| #crate_name::registry::MetaType::Object { name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, fields: { @@ -375,7 +375,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult, ) -> ::std::string::String where Self: #crate_name::OutputType { - registry.create_output_type::(|registry| #crate_name::registry::MetaType::Object { + registry.create_output_type::(#crate_name::registry::MetaTypeId::Object, |registry| #crate_name::registry::MetaType::Object { name: ::std::borrow::ToOwned::to_owned(name), description: #desc, fields: { diff --git a/derive/src/subscription.rs b/derive/src/subscription.rs index ee5b7efe..77120f36 100644 --- a/derive/src/subscription.rs +++ b/derive/src/subscription.rs @@ -92,6 +92,17 @@ pub fn generate( }); use_params.push(quote! { #ident }); + let param_ident = &ident.ident; + let process_with = match argument.process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #param_ident); + } + } + None => Default::default(), + }; + let validators = argument .validator .clone() @@ -102,10 +113,15 @@ pub fn generate( quote!(#ty), Some(quote!(.map_err(|err| err.into_server_error(__pos)))), )?; + let mut non_mut_ident = ident.clone(); + non_mut_ident.mutability = None; get_params.push(quote! { - #[allow(non_snake_case, unused_variables)] - let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?; + #[allow(non_snake_case, unused_variables, unused_mut)] + let (__pos, mut #non_mut_ident) = ctx.oneof_param_value::<#ty>()?; + #process_with #validators + #[allow(non_snake_case, unused_variables)] + let #ident = #non_mut_ident; }); } else { for ( @@ -117,6 +133,7 @@ pub fn generate( default, default_with, validator, + process_with, visible: arg_visible, secret, }, @@ -164,6 +181,18 @@ pub fn generate( } None => quote! { ::std::option::Option::None }, }; + + let param_ident = &ident.ident; + let process_with = match process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(fn_path)?; + quote! { + #fn_path(&mut #param_ident); + } + } + None => Default::default(), + }; + let validators = validator.clone().unwrap_or_default().create_validators( &crate_name, quote!(&#ident), @@ -171,10 +200,15 @@ pub fn generate( Some(quote!(.map_err(|err| err.into_server_error(__pos)))), )?; + let mut non_mut_ident = ident.clone(); + non_mut_ident.mutability = None; get_params.push(quote! { - #[allow(non_snake_case)] - let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; + #[allow(non_snake_case, unused_mut)] + let (__pos, mut #non_mut_ident) = ctx.param_value::<#ty>(#name, #default)?; + #process_with #validators + #[allow(non_snake_case)] + let #ident = #non_mut_ident; }); } } diff --git a/derive/src/union.rs b/derive/src/union.rs index c35bc4bf..62c8d074 100644 --- a/derive/src/union.rs +++ b/derive/src/union.rs @@ -60,9 +60,14 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { } }; - if let Type::Path(p) = &ty { + let mut ty = ty; + while let Type::Group(group) = ty { + ty = &*group.elem; + } + + if matches!(ty, Type::Path(_) | Type::Macro(_)) { // This validates that the field type wasn't already used - if !enum_items.insert(p) { + if !enum_items.insert(ty) { return Err( Error::new_spanned(&ty, "This type already used in another variant").into(), ); @@ -70,16 +75,16 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { enum_names.push(enum_name); - let mut assert_ty = p.clone(); - RemoveLifetime.visit_type_path_mut(&mut assert_ty); + let mut assert_ty = ty.clone(); + RemoveLifetime.visit_type_mut(&mut assert_ty); if !variant.flatten { type_into_impls.push(quote! { #crate_name::static_assertions::assert_impl_one!(#assert_ty: #crate_name::ObjectType); #[allow(clippy::all, clippy::pedantic)] - impl #impl_generics ::std::convert::From<#p> for #ident #ty_generics #where_clause { - fn from(obj: #p) -> Self { + impl #impl_generics ::std::convert::From<#ty> for #ident #ty_generics #where_clause { + fn from(obj: #ty) -> Self { #ident::#enum_name(obj) } } @@ -89,8 +94,8 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { #crate_name::static_assertions::assert_impl_one!(#assert_ty: #crate_name::UnionType); #[allow(clippy::all, clippy::pedantic)] - impl #impl_generics ::std::convert::From<#p> for #ident #ty_generics #where_clause { - fn from(obj: #p) -> Self { + impl #impl_generics ::std::convert::From<#ty> for #ident #ty_generics #where_clause { + fn from(obj: #ty) -> Self { #ident::#enum_name(obj) } } @@ -99,15 +104,15 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { if !variant.flatten { registry_types.push(quote! { - <#p as #crate_name::OutputType>::create_type_info(registry); + <#ty as #crate_name::OutputType>::create_type_info(registry); }); possible_types.push(quote! { - possible_types.insert(<#p as #crate_name::OutputType>::type_name().into_owned()); + possible_types.insert(<#ty as #crate_name::OutputType>::type_name().into_owned()); }); } else { possible_types.push(quote! { if let #crate_name::registry::MetaType::Union { possible_types: possible_types2, .. } = - registry.create_fake_output_type::<#p>() { + registry.create_fake_output_type::<#ty>() { possible_types.extend(possible_types2); } }); @@ -115,11 +120,11 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { if !variant.flatten { get_introspection_typename.push(quote! { - #ident::#enum_name(obj) => <#p as #crate_name::OutputType>::type_name() + #ident::#enum_name(obj) => <#ty as #crate_name::OutputType>::type_name() }); } else { get_introspection_typename.push(quote! { - #ident::#enum_name(obj) => <#p as #crate_name::OutputType>::introspection_type_name(obj) + #ident::#enum_name(obj) => <#ty as #crate_name::OutputType>::introspection_type_name(obj) }); } @@ -172,7 +177,7 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> ::std::string::String { - registry.create_output_type::(|registry| { + registry.create_output_type::(#crate_name::registry::MetaTypeId::Union, |registry| { #(#registry_types)* #crate_name::registry::MetaType::Union { diff --git a/docs/en/src/error_handling.md b/docs/en/src/error_handling.md index 29804042..6cd4080d 100644 --- a/docs/en/src/error_handling.md +++ b/docs/en/src/error_handling.md @@ -25,3 +25,12 @@ impl Query { } } ``` + +#### Errors in subscriptions + +Errors can be returned from subscription resolvers as well, using a return type of the form: +```rust +async fn my_subscription_resolver(&self) -> impl Stream> { ... } +``` + +Note however that the `MyError` struct must have `Clone` implemented, due to the restrictions placed by the `Subscription` macro. One way to accomplish this is by creating a custom error type, with `#[derive(Clone)]`, as [seen here](https://github.com/async-graphql/async-graphql/issues/845#issuecomment-1090933464). diff --git a/examples b/examples index 6a638085..ffb8cb95 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 6a63808523f7e4995f9111a3497f06921cd3b11b +Subproject commit ffb8cb9518e0f23ce17f171c957d3f7948bb1e00 diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index deddaa23..623ba751 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "3.0.36" +version = "3.0.38" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for actix-web" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.36" } +async-graphql = { path = "../..", version = "3.0.38" } actix = "0.13.0" actix-http = "3.0.1" diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 4178ec0d..b32ef45c 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-axum" -version = "3.0.36" +version = "3.0.38" authors = ["sunli "] edition = "2021" description = "async-graphql for axum" @@ -12,14 +12,14 @@ keywords = ["futures", "async", "graphql", "axum"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.36" } +async-graphql = { path = "../..", version = "3.0.38" } async-trait = "0.1.51" -axum = { version = "0.4", features = ["ws", "headers"] } +axum = { version = "0.5.1", features = ["ws", "headers"] } bytes = "1.0.1" http-body = "0.4.2" serde_json = "1.0.66" serde_urlencoded = "0.7.0" -tokio-util = { version = "0.6.7", features = ["io", "compat"] } +tokio-util = { version = "0.7.1", features = ["io", "compat"] } futures-util = "0.3.0" tower-service = "0.3" diff --git a/integrations/axum/src/extract.rs b/integrations/axum/src/extract.rs index 264d48d8..df298cfa 100644 --- a/integrations/axum/src/extract.rs +++ b/integrations/axum/src/extract.rs @@ -106,7 +106,7 @@ where } else { let content_type = req .headers() - .and_then(|headers| headers.get(http::header::CONTENT_TYPE)) + .get(http::header::CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .map(ToString::to_string); let body_stream = BodyStream::from_request(req) diff --git a/integrations/axum/src/subscription.rs b/integrations/axum/src/subscription.rs index 11c3fe4b..9333bb7e 100644 --- a/integrations/axum/src/subscription.rs +++ b/integrations/axum/src/subscription.rs @@ -29,7 +29,7 @@ impl FromRequest for GraphQLProtocol { async fn from_request(req: &mut RequestParts) -> Result { req.headers() - .and_then(|headers| headers.get(http::header::SEC_WEBSOCKET_PROTOCOL)) + .get(http::header::SEC_WEBSOCKET_PROTOCOL) .and_then(|value| value.to_str().ok()) .and_then(|protocols| { protocols diff --git a/integrations/poem/Cargo.toml b/integrations/poem/Cargo.toml index 38f55043..cbcfd117 100644 --- a/integrations/poem/Cargo.toml +++ b/integrations/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-poem" -version = "3.0.36" +version = "3.0.38" authors = ["sunli "] edition = "2021" description = "async-graphql for poem" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "poem"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.36" } +async-graphql = { path = "../..", version = "3.0.38" } poem = { version = "1.2.2", features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false } diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index d87a9f46..0655a4d0 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-rocket" -version = "3.0.36" +version = "3.0.38" authors = ["Daniel Wiesenberg "] edition = "2021" description = "async-graphql for Rocket.rs" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.36" } +async-graphql = { path = "../..", version = "3.0.38" } rocket = { version = "0.5.0-rc.1", default-features = false } serde = "1.0.126" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index e190002a..4b380884 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-tide" -version = "3.0.36" +version = "3.0.38" authors = ["vkill ", "sunli "] edition = "2021" description = "async-graphql for tide" @@ -16,7 +16,7 @@ default = ["websocket"] websocket = ["tide-websockets"] [dependencies] -async-graphql = { path = "../..", version = "3.0.36" } +async-graphql = { path = "../..", version = "3.0.38" } async-trait = "0.1.48" futures-util = "0.3.0" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index be9d7740..de807523 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-warp" -version = "3.0.36" +version = "3.0.38" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for warp" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.36" } +async-graphql = { path = "../..", version = "3.0.38" } warp = { version = "0.3.0", default-features = false, features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false, features = ["sink"] } diff --git a/parser/Cargo.toml b/parser/Cargo.toml index fded82f7..8aa77cec 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-parser" -version = "3.0.36" +version = "3.0.38" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL query parser for async-graphql" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql-value = { path = "../value", version = "3.0.36" } +async-graphql-value = { path = "../value", version = "3.0.38" } pest = "2.1.3" pest_derive = "2.1.0" serde_json = "1.0.64" diff --git a/src/context.rs b/src/context.rs index 39970b15..c1bfc1dd 100644 --- a/src/context.rs +++ b/src/context.rs @@ -13,11 +13,11 @@ use http::HeaderValue; use serde::ser::{SerializeSeq, Serializer}; use serde::Serialize; -use crate::extensions::Extensions; use crate::parser::types::{ Directive, Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet, }; use crate::schema::SchemaEnv; +use crate::{extensions::Extensions, schema::IntrospectionMode}; use crate::{ Error, InputType, Lookahead, Name, OneofObjectType, PathSegment, Pos, Positioned, Result, ServerError, ServerResult, UploadValue, Value, @@ -244,7 +244,7 @@ pub struct QueryEnvInner { pub session_data: Arc, pub ctx_data: Arc, pub http_headers: Mutex, - pub disable_introspection: bool, + pub introspection_mode: IntrospectionMode, pub errors: Mutex>, } diff --git a/src/docs/complex_object.md b/src/docs/complex_object.md index 8cd71b32..25dd991a 100644 --- a/src/docs/complex_object.md +++ b/src/docs/complex_object.md @@ -52,6 +52,7 @@ some simple fields, and use the `ComplexObject` macro to define some other field | 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 | | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y | +| process_with | Upon successful parsing, invokes specified function. Its signature must be `fn(&mut T)`. | code path | Y | # Examples @@ -68,7 +69,7 @@ struct MyObj { #[ComplexObject] impl MyObj { async fn c(&self) -> i32 { - self.a + self.b + self.a + self.b } } diff --git a/src/docs/input_object.md b/src/docs/input_object.md index 5b3cb82f..c93f1d88 100644 --- a/src/docs/input_object.md +++ b/src/docs/input_object.md @@ -24,6 +24,7 @@ Define a GraphQL input object | flatten | Similar to serde (flatten) | boolean | Y | | skip | Skip this field, use `Default::default` to get a default value for this field. | bool | Y | | skip_input | Skip this field, similar to `skip`, but avoids conflicts when this macro is used with `SimpleObject`. | bool | Y | +| process_with | Upon successful parsing, invokes specified function. Its signature must be `fn(&mut T)`. | code path | Y | | 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 | | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y | diff --git a/src/docs/object.md b/src/docs/object.md index fcea0da9..58134b92 100644 --- a/src/docs/object.md +++ b/src/docs/object.md @@ -56,6 +56,7 @@ All methods are converted to camelCase. | 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 | | key | Is entity key(for Federation) | bool | Y | +| process_with | Upon successful parsing, invokes specified function. Its signature must be `fn(&mut T)`. | code path | Y | # Derived argument attributes diff --git a/src/docs/subscription.md b/src/docs/subscription.md index a030846e..bae85730 100644 --- a/src/docs/subscription.md +++ b/src/docs/subscription.md @@ -47,6 +47,7 @@ The filter function should be synchronous. | validator | Input value validator *[See also the Book](https://async-graphql.github.io/async-graphql/en/input_value_validators.html)* | object | Y | | 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 | +| process_with | Upon successful parsing, invokes specified function. Its signature must be `fn(&mut T)`. | code path | Y | # Examples diff --git a/src/http/mod.rs b/src/http/mod.rs index c97e02bf..8adf081e 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -91,8 +91,8 @@ pub async fn receive_batch_json(body: impl AsyncRead) -> Result(&data) - .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e)))?) + serde_json::from_slice::(&data) + .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e))) } /// Receive a GraphQL request from a body as CBOR. @@ -111,6 +111,6 @@ pub async fn receive_batch_cbor(body: impl AsyncRead) -> Result(&data) - .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e)))?) + serde_cbor::from_slice::(&data) + .map_err(|e| ParseRequestError::InvalidRequest(Box::new(e))) } diff --git a/src/http/websocket.rs b/src/http/websocket.rs index e8ad9ffd..1448a96a 100644 --- a/src/http/websocket.rs +++ b/src/http/websocket.rs @@ -365,6 +365,7 @@ impl std::str::FromStr for Protocols { /// A websocket message received from the client #[derive(Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] +#[allow(clippy::large_enum_variant)] //Request is at fault pub enum ClientMessage { /// A new connection ConnectionInit { diff --git a/src/look_ahead.rs b/src/look_ahead.rs index 90844c54..e706e09d 100644 --- a/src/look_ahead.rs +++ b/src/look_ahead.rs @@ -33,13 +33,7 @@ impl<'a> Lookahead<'a> { pub fn field(&self, name: &str) -> Self { let mut fields = Vec::new(); for field in &self.fields { - filter( - &mut fields, - self.fragments, - &field.selection_set.node, - name, - self.context, - ) + filter(&mut fields, self.fragments, &field.selection_set.node, name) } Self { @@ -108,7 +102,6 @@ fn filter<'a>( fragments: &'a HashMap>, selection_set: &'a SelectionSet, name: &str, - context: &'a Context<'a>, ) { for item in &selection_set.items { match &item.node { @@ -117,22 +110,12 @@ fn filter<'a>( fields.push(&field.node) } } - Selection::InlineFragment(fragment) => filter( - fields, - fragments, - &fragment.node.selection_set.node, - name, - context, - ), + Selection::InlineFragment(fragment) => { + filter(fields, fragments, &fragment.node.selection_set.node, name) + } Selection::FragmentSpread(spread) => { if let Some(fragment) = fragments.get(&spread.node.fragment_name.node) { - filter( - fields, - fragments, - &fragment.node.selection_set.node, - name, - context, - ) + filter(fields, fragments, &fragment.node.selection_set.node, name) } } } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index e818e39e..c65d7183 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -3,18 +3,20 @@ mod export_sdl; mod stringify_exec_doc; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt::{self, Display, Formatter}; use indexmap::map::IndexMap; use indexmap::set::IndexSet; pub use crate::model::__DirectiveLocation; -use crate::parser::types::{ - BaseType as ParsedBaseType, Field, Type as ParsedType, VariableDefinition, -}; use crate::{ model, Any, Context, InputType, OutputType, Positioned, ServerResult, SubscriptionType, Value, VisitorContext, }; +use crate::{ + parser::types::{BaseType as ParsedBaseType, Field, Type as ParsedType, VariableDefinition}, + schema::IntrospectionMode, +}; pub use cache_control::CacheControl; @@ -179,6 +181,29 @@ pub struct MetaEnumValue { type MetaVisibleFn = fn(&Context<'_>) -> bool; +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum MetaTypeId { + Scalar, + Object, + Interface, + Union, + Enum, + InputObject, +} + +impl Display for MetaTypeId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(match self { + MetaTypeId::Scalar => "Scalar", + MetaTypeId::Object => "Object", + MetaTypeId::Interface => "Interface", + MetaTypeId::Union => "Union", + MetaTypeId::Enum => "Enum", + MetaTypeId::InputObject => "InputObject", + }) + } +} + #[derive(Clone)] pub enum MetaType { Scalar { @@ -234,6 +259,18 @@ pub enum MetaType { } impl MetaType { + #[inline] + pub fn type_id(&self) -> MetaTypeId { + match self { + MetaType::Scalar { .. } => MetaTypeId::Scalar, + MetaType::Object { .. } => MetaTypeId::Object, + MetaType::Interface { .. } => MetaTypeId::Interface, + MetaType::Union { .. } => MetaTypeId::Union, + MetaType::Enum { .. } => MetaTypeId::Enum, + MetaType::InputObject { .. } => MetaTypeId::InputObject, + } + } + #[inline] pub fn field_by_name(&self, name: &str) -> Option<&MetaField> { self.fields().and_then(|fields| fields.get(name)) @@ -365,36 +402,51 @@ pub struct Registry { pub query_type: String, pub mutation_type: Option, pub subscription_type: Option, - pub disable_introspection: bool, + pub introspection_mode: IntrospectionMode, pub enable_federation: bool, pub federation_subscription: bool, } impl Registry { - pub fn create_input_type MetaType>( - &mut self, - mut f: F, - ) -> String { - self.create_type(&mut f, &*T::type_name(), std::any::type_name::()); + pub fn create_input_type(&mut self, type_id: MetaTypeId, mut f: F) -> String + where + T: InputType + ?Sized, + F: FnMut(&mut Registry) -> MetaType, + { + self.create_type( + &mut f, + &*T::type_name(), + std::any::type_name::(), + type_id, + ); T::qualified_type_name() } - pub fn create_output_type MetaType>( - &mut self, - mut f: F, - ) -> String { - self.create_type(&mut f, &*T::type_name(), std::any::type_name::()); + pub fn create_output_type(&mut self, type_id: MetaTypeId, mut f: F) -> String + where + T: OutputType + ?Sized, + F: FnMut(&mut Registry) -> MetaType, + { + self.create_type( + &mut f, + &*T::type_name(), + std::any::type_name::(), + type_id, + ); T::qualified_type_name() } - pub fn create_subscription_type< + pub fn create_subscription_type(&mut self, mut f: F) -> String + where T: SubscriptionType + ?Sized, F: FnMut(&mut Registry) -> MetaType, - >( - &mut self, - mut f: F, - ) -> String { - self.create_type(&mut f, &*T::type_name(), std::any::type_name::()); + { + self.create_type( + &mut f, + &*T::type_name(), + std::any::type_name::(), + MetaTypeId::Object, + ); T::qualified_type_name() } @@ -403,16 +455,30 @@ impl Registry { f: &mut F, name: &str, rust_typename: &str, + type_id: MetaTypeId, ) { match self.types.get(name) { Some(ty) => { if let Some(prev_typename) = ty.rust_typename() { - if prev_typename != "__fake_type__" && rust_typename != prev_typename { + if prev_typename == "__fake_type__" { + return; + } + + if rust_typename != prev_typename { panic!( "`{}` and `{}` have the same GraphQL name `{}`", prev_typename, rust_typename, name, ); } + + if ty.type_id() != type_id { + panic!( + "Register `{}` as `{}`, but it is already registered as `{}`", + name, + type_id, + ty.type_id() + ); + } } } None => { diff --git a/src/request.rs b/src/request.rs index 9132e7af..4b04afba 100644 --- a/src/request.rs +++ b/src/request.rs @@ -4,7 +4,10 @@ use std::fmt::{self, Debug, Formatter}; use serde::{Deserialize, Deserializer, Serialize}; -use crate::{Data, ParseRequestError, UploadValue, Value, Variables}; +use crate::parser::parse_query; +use crate::parser::types::ExecutableDocument; +use crate::schema::IntrospectionMode; +use crate::{Data, ParseRequestError, ServerError, UploadValue, Value, Variables}; /// GraphQL request. /// @@ -12,6 +15,7 @@ use crate::{Data, ParseRequestError, UploadValue, Value, Variables}; /// variables. The names are all in `camelCase` (e.g. `operationName`). #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] +#[non_exhaustive] pub struct Request { /// The query source of the request. #[serde(default)] @@ -40,8 +44,17 @@ pub struct Request { pub extensions: HashMap, /// Disable introspection queries for this request. + /// This option has priority over `introspection_mode` when set to true. + /// `introspection_mode` has priority when `disable_introspection` set to `false`. #[serde(skip)] pub disable_introspection: bool, + + #[serde(skip)] + pub(crate) parsed_query: Option, + + /// Sets the introspection mode for this request (defaults to [IntrospectionMode::Enabled]). + #[serde(skip)] + pub introspection_mode: IntrospectionMode, } impl Request { @@ -55,6 +68,8 @@ impl Request { data: Data::default(), extensions: Default::default(), disable_introspection: false, + parsed_query: None, + introspection_mode: IntrospectionMode::Enabled, } } @@ -84,9 +99,36 @@ impl Request { #[must_use] pub fn disable_introspection(mut self) -> Self { self.disable_introspection = true; + self.introspection_mode = IntrospectionMode::Disabled; self } + /// Only allow introspection queries for this request. + #[must_use] + pub fn only_introspection(mut self) -> Self { + self.disable_introspection = false; + self.introspection_mode = IntrospectionMode::IntrospectionOnly; + self + } + + #[inline] + /// Performs parsing of query ahead of execution. + /// + /// This effectively allows to inspect query information, before passing request to schema for + /// execution as long as query is valid. + pub fn parsed_query(&mut self) -> Result<&ExecutableDocument, ServerError> { + if self.parsed_query.is_none() { + match parse_query(&self.query) { + Ok(parsed) => self.parsed_query = Some(parsed), + Err(error) => return Err(error.into()), + } + } + + //forbid_unsafe effectively bans optimize away else branch here so use unwrap + //but this unwrap never panics + Ok(self.parsed_query.as_ref().unwrap()) + } + /// Set a variable to an upload value. /// /// `var_path` is a dot-separated path to the item that begins with `variables`, for example @@ -141,6 +183,7 @@ impl Debug for Request { /// **Reference:** #[derive(Debug, Deserialize)] #[serde(untagged)] +#[allow(clippy::large_enum_variant)] //Request is at fault pub enum BatchRequest { /// Single query Single(Request), @@ -207,6 +250,17 @@ impl BatchRequest { pub fn disable_introspection(mut self) -> Self { for request in self.iter_mut() { request.disable_introspection = true; + request.introspection_mode = IntrospectionMode::Disabled; + } + self + } + + /// Only allow introspection queries for each request. + #[must_use] + pub fn introspection_only(mut self) -> Self { + for request in self.iter_mut() { + request.disable_introspection = false; + request.introspection_mode = IntrospectionMode::IntrospectionOnly; } self } diff --git a/src/resolver_utils/scalar.rs b/src/resolver_utils/scalar.rs index f17b01c4..85dd9c27 100644 --- a/src/resolver_utils/scalar.rs +++ b/src/resolver_utils/scalar.rs @@ -154,12 +154,14 @@ macro_rules! scalar_internal { fn create_type_info( registry: &mut $crate::registry::Registry, ) -> ::std::string::String { - registry.create_input_type::<$ty, _>(|_| $crate::registry::MetaType::Scalar { - name: ::std::borrow::ToOwned::to_owned($name), - description: $desc, - is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value), - visible: ::std::option::Option::None, - specified_by_url: $specified_by_url, + registry.create_input_type::<$ty, _>($crate::registry::MetaTypeId::Scalar, |_| { + $crate::registry::MetaType::Scalar { + name: ::std::borrow::ToOwned::to_owned($name), + description: $desc, + is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value), + visible: ::std::option::Option::None, + specified_by_url: $specified_by_url, + } }) } @@ -187,12 +189,14 @@ macro_rules! scalar_internal { fn create_type_info( registry: &mut $crate::registry::Registry, ) -> ::std::string::String { - registry.create_output_type::<$ty, _>(|_| $crate::registry::MetaType::Scalar { - name: ::std::borrow::ToOwned::to_owned($name), - description: $desc, - is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value), - visible: ::std::option::Option::None, - specified_by_url: $specified_by_url, + registry.create_output_type::<$ty, _>($crate::registry::MetaTypeId::Scalar, |_| { + $crate::registry::MetaType::Scalar { + name: ::std::borrow::ToOwned::to_owned($name), + description: $desc, + is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value), + visible: ::std::option::Option::None, + specified_by_url: $specified_by_url, + } }) } diff --git a/src/schema.rs b/src/schema.rs index bfdd0920..93fb7fe4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -6,7 +6,6 @@ use std::sync::Arc; use futures_util::stream::{self, Stream, StreamExt}; use indexmap::map::IndexMap; -use crate::context::{Data, QueryEnvInner}; use crate::custom_directive::CustomDirectiveFactory; use crate::extensions::{ExtensionFactory, Extensions}; use crate::model::__DirectiveLocation; @@ -17,11 +16,29 @@ use crate::resolver_utils::{resolve_container, resolve_container_serial}; use crate::subscription::collect_subscription_streams; use crate::types::QueryRoot; use crate::validation::{check_rules, ValidationMode}; +use crate::{ + context::{Data, QueryEnvInner}, + EmptyMutation, EmptySubscription, +}; use crate::{ BatchRequest, BatchResponse, CacheControl, ContextBase, InputType, ObjectType, OutputType, QueryEnv, Request, Response, ServerError, SubscriptionType, Variables, ID, }; +/// Introspection mode +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum IntrospectionMode { + IntrospectionOnly, + Enabled, + Disabled, +} + +impl Default for IntrospectionMode { + fn default() -> Self { + IntrospectionMode::Enabled + } +} + /// Schema builder pub struct SchemaBuilder { validation_mode: ValidationMode, @@ -58,7 +75,14 @@ impl SchemaBuilder /// Disable introspection queries. #[must_use] pub fn disable_introspection(mut self) -> Self { - self.registry.disable_introspection = true; + self.registry.introspection_mode = IntrospectionMode::Disabled; + self + } + + /// Only process introspection queries, everything else is processed as an error. + #[must_use] + pub fn introspection_only(mut self) -> Self { + self.registry.introspection_mode = IntrospectionMode::IntrospectionOnly; self } @@ -303,7 +327,7 @@ where } else { Some(Subscription::type_name().to_string()) }, - disable_introspection: false, + introspection_mode: IntrospectionMode::Enabled, enable_federation: false, federation_subscription: false, }; @@ -427,13 +451,19 @@ where let query_data = Arc::new(std::mem::take(&mut request.data)); extensions.attach_query_data(query_data.clone()); - let request = extensions.prepare_request(request).await?; + let mut request = extensions.prepare_request(request).await?; let mut document = { let query = &request.query; - let fut_parse = async { parse_query(&query).map_err(Into::::into) }; + let parsed_doc = request.parsed_query.take(); + let fut_parse = async move { + match parsed_doc { + Some(parsed_doc) => Ok(parsed_doc), + None => parse_query(query).map_err(Into::into), + } + }; futures_util::pin_mut!(fut_parse); extensions - .parse_query(&query, &request.variables, &mut fut_parse) + .parse_query(query, &request.variables, &mut fut_parse) .await? }; @@ -509,7 +539,11 @@ where session_data, ctx_data: query_data, http_headers: Default::default(), - disable_introspection: request.disable_introspection, + introspection_mode: if request.disable_introspection { + IntrospectionMode::Disabled + } else { + request.introspection_mode + }, errors: Default::default(), }; Ok((QueryEnv::new(env), validation_result.cache_control)) @@ -526,7 +560,15 @@ where let res = match &env.operation.node.ty { OperationType::Query => resolve_container(&ctx, &self.query).await, - OperationType::Mutation => resolve_container_serial(&ctx, &self.mutation).await, + OperationType::Mutation => { + if self.env.registry.introspection_mode == IntrospectionMode::IntrospectionOnly + || env.introspection_mode == IntrospectionMode::IntrospectionOnly + { + resolve_container_serial(&ctx, &EmptyMutation).await + } else { + resolve_container_serial(&ctx, &self.mutation).await + } + } OperationType::Subscription => Err(ServerError::new( "Subscriptions are not supported on this transport.", None, @@ -621,7 +663,15 @@ where ); let mut streams = Vec::new(); - if let Err(err) = collect_subscription_streams(&ctx, &schema.subscription, &mut streams) { + let collect_result = if schema.env.registry.introspection_mode + == IntrospectionMode::IntrospectionOnly + || env.introspection_mode == IntrospectionMode::IntrospectionOnly + { + collect_subscription_streams(&ctx, &EmptySubscription, &mut streams) + } else { + collect_subscription_streams(&ctx, &schema.subscription, &mut streams) + }; + if let Err(err) = collect_result { yield Response::from_errors(vec![err]); } diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index 52bd2fcf..e17c8340 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -6,6 +6,7 @@ use indexmap::map::IndexMap; use crate::connection::edge::Edge; use crate::connection::page_info::PageInfo; use crate::parser::types::Field; +use crate::registry::MetaTypeId; use crate::resolver_utils::{resolve_container, ContainerType}; use crate::types::connection::{CursorType, EmptyFields}; use crate::{ @@ -163,7 +164,7 @@ where } fn create_type_info(registry: &mut registry::Registry) -> String { - registry.create_output_type::(|registry| { + registry.create_output_type::(MetaTypeId::Object,|registry| { EC::create_type_info(registry); let additional_fields = if let Some(registry::MetaType::Object { fields, .. }) = registry.types.remove(EC::type_name().as_ref()) diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index b4704f05..54f4660a 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -4,6 +4,7 @@ use indexmap::map::IndexMap; use crate::connection::EmptyFields; use crate::parser::types::Field; +use crate::registry::MetaTypeId; use crate::resolver_utils::{resolve_container, ContainerType}; use crate::types::connection::CursorType; use crate::{ @@ -72,7 +73,7 @@ where } fn create_type_info(registry: &mut registry::Registry) -> String { - registry.create_output_type::(|registry| { + registry.create_output_type::(MetaTypeId::Object, |registry| { let additional_fields = if let registry::MetaType::Object { fields, .. } = registry.create_fake_output_type::() { diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 7b5f2f3c..16a684ce 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use crate::parser::types::Field; +use crate::registry::MetaTypeId; use crate::resolver_utils::ContainerType; use crate::{ registry, Context, ContextSelectionSet, ObjectType, OutputType, Positioned, ServerError, @@ -49,7 +50,7 @@ impl OutputType for EmptyMutation { } fn create_type_info(registry: &mut registry::Registry) -> String { - registry.create_output_type::(|_| registry::MetaType::Object { + registry.create_output_type::(MetaTypeId::Object, |_| registry::MetaType::Object { name: "EmptyMutation".to_string(), description: None, fields: Default::default(), diff --git a/src/types/external/json_object/btreemap.rs b/src/types/external/json_object/btreemap.rs index 9df8a2c6..221e78d1 100644 --- a/src/types/external/json_object/btreemap.rs +++ b/src/types/external/json_object/btreemap.rs @@ -10,7 +10,7 @@ use indexmap::IndexMap; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::registry::{MetaType, Registry}; +use crate::registry::{MetaType, MetaTypeId, Registry}; use crate::{ ContextSelectionSet, InputType, InputValueError, InputValueResult, Name, OutputType, ServerResult, Value, @@ -29,7 +29,7 @@ where } fn create_type_info(registry: &mut Registry) -> String { - registry.create_input_type::(|_| MetaType::Scalar { + registry.create_input_type::(MetaTypeId::Scalar, |_| MetaType::Scalar { name: ::type_name().to_string(), description: Some("A scalar that can represent any JSON Object value."), is_valid: |_| true, @@ -84,7 +84,7 @@ where } fn create_type_info(registry: &mut Registry) -> String { - registry.create_output_type::(|_| MetaType::Scalar { + registry.create_output_type::(MetaTypeId::Scalar, |_| MetaType::Scalar { name: ::type_name().to_string(), description: Some("A scalar that can represent any JSON Object value."), is_valid: |_| true, diff --git a/src/types/external/json_object/hashmap.rs b/src/types/external/json_object/hashmap.rs index 09f96450..1df5caf9 100644 --- a/src/types/external/json_object/hashmap.rs +++ b/src/types/external/json_object/hashmap.rs @@ -11,7 +11,7 @@ use indexmap::IndexMap; use serde::de::DeserializeOwned; use serde::Serialize; -use crate::registry::{MetaType, Registry}; +use crate::registry::{MetaType, MetaTypeId, Registry}; use crate::{ ContextSelectionSet, InputType, InputValueError, InputValueResult, Name, OutputType, ServerResult, Value, @@ -31,7 +31,7 @@ where } fn create_type_info(registry: &mut Registry) -> String { - registry.create_input_type::(|_| MetaType::Scalar { + registry.create_input_type::(MetaTypeId::Scalar, |_| MetaType::Scalar { name: ::type_name().to_string(), description: Some("A scalar that can represent any JSON Object value."), is_valid: |_| true, @@ -87,7 +87,7 @@ where } fn create_type_info(registry: &mut Registry) -> String { - registry.create_output_type::(|_| MetaType::Scalar { + registry.create_output_type::(MetaTypeId::Scalar, |_| MetaType::Scalar { name: ::type_name().to_string(), description: Some("A scalar that can represent any JSON Object value."), is_valid: |_| true, diff --git a/src/types/json.rs b/src/types/json.rs index e80fa089..89971ccd 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -5,7 +5,7 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use crate::parser::types::Field; -use crate::registry::{MetaType, Registry}; +use crate::registry::{MetaType, MetaTypeId, Registry}; use crate::{ from_value, to_value, ContextSelectionSet, InputType, InputValueResult, OutputType, Positioned, ServerResult, Value, @@ -46,7 +46,7 @@ impl InputType for Json { } fn create_type_info(registry: &mut Registry) -> String { - registry.create_input_type::, _>(|_| MetaType::Scalar { + registry.create_input_type::, _>(MetaTypeId::Scalar, |_| MetaType::Scalar { name: ::type_name().to_string(), description: Some("A scalar that can represent any JSON value."), is_valid: |_| true, @@ -75,7 +75,7 @@ impl OutputType for Json { } fn create_type_info(registry: &mut Registry) -> String { - registry.create_output_type::, _>(|_| MetaType::Scalar { + registry.create_output_type::, _>(MetaTypeId::Scalar, |_| MetaType::Scalar { name: ::type_name().to_string(), description: Some("A scalar that can represent any JSON value."), is_valid: |_| true, @@ -101,12 +101,14 @@ impl InputType for serde_json::Value { } fn create_type_info(registry: &mut Registry) -> String { - registry.create_input_type::(|_| MetaType::Scalar { - name: ::type_name().to_string(), - description: Some("A scalar that can represent any JSON value."), - is_valid: |_| true, - visible: None, - specified_by_url: None, + registry.create_input_type::(MetaTypeId::Scalar, |_| { + MetaType::Scalar { + name: ::type_name().to_string(), + description: Some("A scalar that can represent any JSON value."), + is_valid: |_| true, + visible: None, + specified_by_url: None, + } }) } @@ -130,12 +132,14 @@ impl OutputType for serde_json::Value { } fn create_type_info(registry: &mut Registry) -> String { - registry.create_output_type::(|_| MetaType::Scalar { - name: ::type_name().to_string(), - description: Some("A scalar that can represent any JSON value."), - is_valid: |_| true, - visible: None, - specified_by_url: None, + registry.create_output_type::(MetaTypeId::Scalar, |_| { + MetaType::Scalar { + name: ::type_name().to_string(), + description: Some("A scalar that can represent any JSON value."), + is_valid: |_| true, + visible: None, + specified_by_url: None, + } }) } diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 2bc99633..6deb7e3c 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -172,6 +172,32 @@ impl MaybeUndefined { MaybeUndefined::Undefined => MaybeUndefined::Undefined, } } + + /// Update `value` if the `MaybeUndefined` is not undefined. + /// + /// # Example + /// + /// ```rust + /// use async_graphql::MaybeUndefined; + /// + /// let mut value = None; + /// + /// MaybeUndefined::Value(10i32).update_to(&mut value); + /// assert_eq!(value, Some(10)); + /// + /// MaybeUndefined::Undefined.update_to(&mut value); + /// assert_eq!(value, Some(10)); + /// + /// MaybeUndefined::Null.update_to(&mut value); + /// assert_eq!(value, None); + /// ``` + pub fn update_to(self, value: &mut Option) { + match self { + MaybeUndefined::Value(new) => *value = Some(new), + MaybeUndefined::Null => *value = None, + MaybeUndefined::Undefined => {} + }; + } } impl InputType for MaybeUndefined { diff --git a/src/types/merged_object.rs b/src/types/merged_object.rs index b6ff21fd..09fe1b46 100644 --- a/src/types/merged_object.rs +++ b/src/types/merged_object.rs @@ -5,7 +5,7 @@ use indexmap::IndexMap; use crate::futures_util::Stream; use crate::parser::types::Field; -use crate::registry::{MetaType, Registry}; +use crate::registry::{MetaType, MetaTypeId, Registry}; use crate::{ CacheControl, ContainerType, Context, ContextSelectionSet, OutputType, Positioned, Response, ServerResult, SimpleObject, SubscriptionType, Value, @@ -48,7 +48,7 @@ where } fn create_type_info(registry: &mut Registry) -> String { - registry.create_output_type::(|registry| { + registry.create_output_type::(MetaTypeId::Object, |registry| { let mut fields = IndexMap::new(); let mut cc = CacheControl::default(); diff --git a/src/types/query_root.rs b/src/types/query_root.rs index d984b965..f8671144 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -2,9 +2,12 @@ use std::borrow::Cow; use indexmap::map::IndexMap; -use crate::model::{__Schema, __Type}; use crate::parser::types::Field; use crate::resolver_utils::{resolve_container, ContainerType}; +use crate::{ + model::{__Schema, __Type}, + schema::IntrospectionMode, +}; use crate::{ registry, Any, Context, ContextSelectionSet, ObjectType, OutputType, Positioned, ServerError, ServerResult, SimpleObject, Value, @@ -24,7 +27,13 @@ pub(crate) struct QueryRoot { #[async_trait::async_trait] impl ContainerType for QueryRoot { async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult> { - if !ctx.schema_env.registry.disable_introspection && !ctx.query_env.disable_introspection { + if matches!( + ctx.schema_env.registry.introspection_mode, + IntrospectionMode::Enabled | IntrospectionMode::IntrospectionOnly + ) && matches!( + ctx.query_env.introspection_mode, + IntrospectionMode::Enabled | IntrospectionMode::IntrospectionOnly, + ) { if ctx.item.node.name.node == "__schema" { let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set); let visible_types = ctx.schema_env.registry.find_visible_types(ctx); @@ -54,6 +63,12 @@ impl ContainerType for QueryRoot { } } + if ctx.schema_env.registry.introspection_mode == IntrospectionMode::IntrospectionOnly + || ctx.query_env.introspection_mode == IntrospectionMode::IntrospectionOnly + { + return Ok(None); + } + if ctx.schema_env.registry.enable_federation || ctx.schema_env.registry.has_entities() { if ctx.item.node.name.node == "_entities" { let (_, representations) = ctx.param_value::>("representations", None)?; @@ -93,7 +108,10 @@ impl OutputType for QueryRoot { fn create_type_info(registry: &mut registry::Registry) -> String { let root = T::create_type_info(registry); - if !registry.disable_introspection { + if matches!( + registry.introspection_mode, + IntrospectionMode::Enabled | IntrospectionMode::IntrospectionOnly + ) { let schema_type = __Schema::create_type_info(registry); if let Some(registry::MetaType::Object { fields, .. }) = registry.types.get_mut(T::type_name().as_ref()) diff --git a/src/types/upload.rs b/src/types/upload.rs index 23df60e3..06b9ed06 100644 --- a/src/types/upload.rs +++ b/src/types/upload.rs @@ -5,6 +5,7 @@ use std::io::Read; #[cfg(feature = "unblock")] use futures_util::io::AsyncRead; +use crate::registry::MetaTypeId; use crate::{registry, Context, InputType, InputValueError, InputValueResult, Value}; /// A file upload value. @@ -109,7 +110,7 @@ impl InputType for Upload { } fn create_type_info(registry: &mut registry::Registry) -> String { - registry.create_input_type::(|_| registry::MetaType::Scalar { + registry.create_input_type::(MetaTypeId::Scalar, |_| registry::MetaType::Scalar { name: Self::type_name().to_string(), description: None, is_valid: |value| matches!(value, Value::String(_)), diff --git a/tests/complex_object.rs b/tests/complex_object.rs index f9e3d90f..6337979b 100644 --- a/tests/complex_object.rs +++ b/tests/complex_object.rs @@ -1,6 +1,45 @@ use async_graphql::*; use core::marker::PhantomData; +#[tokio::test] +async fn test_complex_object_process_with_method_field() { + #[derive(SimpleObject)] + #[graphql(complex)] + struct MyObj { + a: i32, + } + + #[ComplexObject] + impl MyObj { + async fn test( + &self, + #[graphql(process_with = "str::make_ascii_uppercase")] processed_complex_arg: String, + ) -> String { + processed_complex_arg + } + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self) -> MyObj { + MyObj { a: 10 } + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + let query = "{ obj { test(processedComplexArg: \"smol\") } }"; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "obj": { + "test": "SMOL" + } + }) + ); +} + #[tokio::test] pub async fn test_complex_object() { #[derive(SimpleObject)] diff --git a/tests/input_object.rs b/tests/input_object.rs index 92019b22..acc3eb44 100644 --- a/tests/input_object.rs +++ b/tests/input_object.rs @@ -407,6 +407,31 @@ pub async fn test_both_input_output() { assert_eq!(::type_name(), "MyObject"); } +#[test] +#[should_panic] +pub fn test_both_input_output_with_same_name() { + #[derive(SimpleObject, InputObject)] + #[allow(dead_code)] + struct MyObject { + #[graphql(default = 10)] + a: i32, + b: bool, + #[graphql(skip)] + c: String, + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self, input: MyObject) -> MyObject { + input + } + } + + Schema::new(Query, EmptyMutation, EmptySubscription); +} + #[tokio::test] pub async fn test_skip_input() { #[derive(SimpleObject, InputObject)] @@ -521,3 +546,88 @@ pub async fn test_complex_output() { }) ); } + +#[tokio::test] +pub async fn test_input_object_process_with() { + mod processor { + pub fn string(input: &mut String) { + while let Some(ch) = input.pop() { + if !ch.is_whitespace() { + input.push(ch); + break; + } + } + } + } + #[derive(InputObject)] + struct MyInput { + //processor does nothing on default value + #[graphql(default = " ", process_with = "processor::string")] + a: String, + + #[graphql(process_with = "processor::string")] + b: String, + } + + struct MyOutput { + a: String, + b: String, + } + + #[Object] + impl MyOutput { + async fn a(&self) -> &String { + &self.a + } + + async fn b(&self) -> &String { + &self.b + } + } + + struct Root; + + #[Object] + impl Root { + async fn a(&self, input: MyInput) -> MyOutput { + MyOutput { + a: input.a, + b: input.b, + } + } + } + + let schema = Schema::new(Root, EmptyMutation, EmptySubscription); + let query = r#"{ + a(input:{b: "test b "}) { + a b + } + }"# + .to_owned(); + assert_eq!( + schema.execute(&query).await.data, + value!({ + "a": { + "a": " ", + "b": "test b", + } + }) + ); + + let schema = Schema::new(Root, EmptyMutation, EmptySubscription); + let query = r#"{ + a(input:{a: "test a ", b: "test"}) { + a b + } + }"# + .to_owned(); + assert_eq!( + schema.execute(&query).await.data, + value!({ + "a": { + "a": "test a", + "b": "test", + } + }) + ); +} diff --git a/tests/introspection.rs b/tests/introspection.rs index 22832785..6eb2d90f 100644 --- a/tests/introspection.rs +++ b/tests/introspection.rs @@ -1272,3 +1272,109 @@ pub async fn test_disable_introspection() { value!({ "__type": null }) ); } + +#[tokio::test] +pub async fn test_introspection_only() { + let schema = Schema::build(Query, Mutation, EmptySubscription) + .introspection_only() + .finish(); + + // Test whether introspection works. + let query = r#" + { + __type(name: "Mutation") { + name + kind + description + fields { + description + name + type { kind name } + args { name } + } + } + } + "#; + let res_json = value!({ + "__type": { + "name": "Mutation", + "kind": "OBJECT", + "description": "Global mutation", + "fields": [ + { + "description": "simple_mutation description\nline2\nline3", + "name": "simpleMutation", + "type": { + "kind": "NON_NULL", + "name": null + }, + "args": [ + { + "name": "input" + } + ] + } + ] + } + }); + let res = schema.execute(query).await.into_result().unwrap().data; + assert_eq!(res, res_json); + + // Test whether introspection works. + let query = r#" + { + __type(name: "Query") { + name + kind + description + fields { + description + name + type { kind name } + args { name } + } + } + } + "#; + let res_json = value!({ + "__type": { + "name": "Query", + "kind": "OBJECT", + "description": "Global query", + "fields": [ + { + "description": "Get a simple object", + "name": "simpleObject", + "type": { "kind": "NON_NULL", "name": null }, + "args": [] + } + ] + } + }); + let res = schema.execute(query).await.into_result().unwrap().data; + assert_eq!(res, res_json); + + // Queries shouldn't work in introspection only mode. + let query = r#" + { + simpleObject { + a + } + } + "#; + let res_json = value!({ "simpleObject": null }); + let res = schema.execute(query).await.into_result().unwrap().data; + assert_eq!(res, res_json); + + // Mutations shouldn't work in introspection only mode. + let query = r#" + mutation { + simpleMutation(input: { a: "" }) { + a + } + } + "#; + let res_json = value!({ "simpleMutation": null }); + let res = schema.execute(query).await.into_result().unwrap().data; + assert_eq!(res, res_json); +} diff --git a/tests/object.rs b/tests/object.rs index 71377b15..70394d3a 100644 --- a/tests/object.rs +++ b/tests/object.rs @@ -118,6 +118,30 @@ async fn test_flatten_with_context() { ); } +#[tokio::test] +async fn test_object_process_with_field() { + struct Query; + + #[Object] + impl Query { + async fn test( + &self, + #[graphql(process_with = "str::make_ascii_uppercase")] processed_arg: String, + ) -> String { + processed_arg + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + let query = "{ test(processedArg: \"smol\") }"; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "test": "SMOL" + }) + ); +} + #[tokio::test] async fn test_oneof_field() { #[derive(OneofObject)] diff --git a/tests/union.rs b/tests/union.rs index 4de78dee..06db6ae0 100644 --- a/tests/union.rs +++ b/tests/union.rs @@ -419,3 +419,24 @@ pub async fn test_trait_object_in_union() { }) ); } + +macro_rules! generate_union { + ($name:ident, $variant_ty:ty) => { + #[derive(Union)] + pub enum $name { + Val($variant_ty), + } + }; +} + +#[test] +pub fn test_macro_generated_union() { + #[derive(SimpleObject)] + pub struct IntObj { + pub val: i32, + } + + generate_union!(MyEnum, IntObj); + + let _ = MyEnum::Val(IntObj { val: 1 }); +} diff --git a/value/Cargo.toml b/value/Cargo.toml index 94e69eed..8682e59c 100644 --- a/value/Cargo.toml +++ b/value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-value" -version = "3.0.36" +version = "3.0.38" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL value for async-graphql"