From eb438ffec9c3ddb42d49d5862190a8e73d44e4b5 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Sun, 12 Apr 2020 19:35:25 +0200 Subject: [PATCH 01/11] Implemented device macro for GraphQLUnion's --- .../juniper_tests/src/codegen/derive_union.rs | 92 ++++++++ .../juniper_tests/src/codegen/mod.rs | 1 + juniper/CHANGELOG.md | 5 + juniper/src/lib.rs | 2 +- juniper_codegen/src/derive_union.rs | 99 ++++++++ juniper_codegen/src/lib.rs | 8 + juniper_codegen/src/util/mod.rs | 213 ++++++++++++++++++ 7 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 integration_tests/juniper_tests/src/codegen/derive_union.rs create mode 100644 juniper_codegen/src/derive_union.rs diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs new file mode 100644 index 000000000..7bae5dd6a --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -0,0 +1,92 @@ +// Default Test +#[derive(juniper::GraphQLObject)] +pub struct Human { + id: String, + home_planet: String, +} + +#[derive(juniper::GraphQLObject)] +pub struct Droid { + id: String, + primary_function: String, +} + +#[derive(juniper::GraphQLUnion)] +pub enum Character { + One(Human), + Two(Droid), +} + +// Context Test +pub struct CustomContext; + +impl juniper::Context for CustomContext {} + +#[derive(juniper::GraphQLObject)] +#[graphql(Context = CustomContext)] +pub struct HumanContext { + id: String, + home_planet: String, +} + +#[derive(juniper::GraphQLObject)] +#[graphql(Context = CustomContext)] +pub struct DroidContext { + id: String, + primary_function: String, +} + +#[derive(juniper::GraphQLUnion)] +#[graphql(Context = CustomContext)] +pub enum CharacterContext { + One(HumanContext), + Two(DroidContext), +} + +// #[juniper::object] compatibility + +pub struct HumanCompat { + id: String, + home_planet: String, +} + +#[juniper::graphql_object] +impl HumanCompat { + fn id(&self) -> &String { + &self.id + } + + fn home_planet(&self) -> &String { + &self.home_planet + } +} + +pub struct DroidCompat { + id: String, + primary_function: String, +} + +#[juniper::graphql_object] +impl DroidCompat { + fn id(&self) -> &String { + &self.id + } + + fn primary_function(&self) -> &String { + &self.primary_function + } +} + +// NOTICE: this can not compile +// #[derive(juniper::GraphQLUnion)] +// pub enum CharacterCompatFail { +// One(HumanCompat), +// Two(DroidCompat), +// } + +#[derive(juniper::GraphQLUnion)] +#[graphql(Scalar = juniper::DefaultScalarValue)] +pub enum CharacterCompat { + One(HumanCompat), + Two(DroidCompat), +} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 1f5a2bb04..8c3baeb1b 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -2,5 +2,6 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_object_with_raw_idents; +mod derive_union; mod impl_union; mod scalar_value_transparent; diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 6f5afa1fe..b0d91101f 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -20,6 +20,11 @@ See [#419](https://github.com/graphql-rust/juniper/pull/419). See [#569](https://github.com/graphql-rust/juniper/pull/569). +- GraphQLUnion derive support ("#[derive(GraphqQLUnion)]") + - implements GraphQLAsyncType + +See [#](https://github.com/graphql-rust/juniper/pull/). + ## Breaking Changes - `juniper::graphiql` has moved to `juniper::http::graphiql` diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 4f08a0999..30e5edc47 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -116,7 +116,7 @@ extern crate bson; // functionality automatically. pub use juniper_codegen::{ graphql_object, graphql_subscription, graphql_union, GraphQLEnum, GraphQLInputObject, - GraphQLObject, GraphQLScalarValue, + GraphQLObject, GraphQLScalarValue, GraphQLUnion, }; // Internal macros are not exported, // but declared at the root to make them easier to use. diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs new file mode 100644 index 000000000..6a03c271a --- /dev/null +++ b/juniper_codegen/src/derive_union.rs @@ -0,0 +1,99 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{self, Data, Fields}; + +use crate::util; + +pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStream { + let enum_fields = match ast.data { + Data::Enum(data) => data.variants, + _ => { + panic!("#[derive(GraphlQLUnion)] can only be applied to enums"); + } + }; + + // Parse attributes. + let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) { + Ok(a) => a, + Err(e) => { + panic!("Invalid #[graphql(...)] attribute for enum: {}", e); + } + }; + + if !attrs.interfaces.is_empty() { + panic!("#[derive(GraphlQLUnion)] does not support interfaces"); + } + + let ident = &ast.ident; + let name = attrs.name.unwrap_or_else(|| ident.to_string()); + + let fields = enum_fields.into_iter().filter_map(|field| { + let field_attrs = match util::FieldAttributes::from_attrs( + field.attrs, + util::FieldAttributeParseMode::Object, + ) { + Ok(attrs) => attrs, + Err(e) => panic!("Invalid #[graphql] attribute for field: \n{}", e), + }; + + + if field_attrs.skip { + None + } else { + let field_name = field.ident; + let name = field_attrs + .name + .clone() + .unwrap_or_else(|| util::to_camel_case(&field_name.to_string())); + + let resolver_code = quote!( + #ident . #field_name + ); + + let _type = match field.fields { + Fields::Unnamed(inner) => { + let mut iter = inner.unnamed.iter(); + let first = match iter.next() { + Some(val) => val, + None => unreachable!(), + }; + + if iter.next().is_some() { + panic!("#[derive(GraphlQLUnion)] all members must be unnamed with a single element e.g. Some(T)"); + } + + first.ty.clone() + } + _ => panic!("#[derive(GraphlQLObject)] all fields of the enum must be unnamed"), + }; + + Some(util::GraphQLTypeDefinitionField { + name, + _type, + args: Vec::new(), + description: field_attrs.description, + deprecation: field_attrs.deprecation, + resolver_code, + is_type_inferred: true, + is_async: false, + }) + } + }); + + let definition = util::GraphQLTypeDefiniton { + name, + _type: syn::parse_str(&ast.ident.to_string()).unwrap(), + context: attrs.context, + scalar: attrs.scalar, + description: attrs.description, + fields: fields.collect(), + generics: ast.generics, + interfaces: None, + include_type_generics: true, + generic_scalar: true, + no_async: attrs.no_async, + }; + + let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; + definition.into_union_tokens(juniper_crate_name) +} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 562e4a2f7..923519a45 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -15,6 +15,7 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_scalar_value; +mod derive_union; mod impl_object; mod impl_union; @@ -63,6 +64,13 @@ pub fn derive_object_internal(input: TokenStream) -> TokenStream { let gen = derive_object::build_derive_object(ast, true); gen.into() } + +#[proc_macro_derive(GraphQLUnion, attributes(graphql))] +pub fn derive_union(input: TokenStream) -> TokenStream { + let ast = syn::parse::(input).unwrap(); + let gen = derive_union::build_derive_union(ast, false); + gen.into() +} /// This custom derive macro implements the #[derive(GraphQLScalarValue)] /// derive. /// diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index b9969e202..593517b92 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1255,6 +1255,219 @@ impl GraphQLTypeDefiniton { #subscription_implementation ) } + + pub fn into_union_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream { + let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); + + let name = &self.name; + let ty = &self._type; + let context = self + .context + .as_ref() + .map(|ctx| quote!( #ctx )) + .unwrap_or_else(|| quote!(())); + + let scalar = self + .scalar + .as_ref() + .map(|s| quote!( #s )) + .unwrap_or_else(|| { + if self.generic_scalar { + // If generic_scalar is true, we always insert a generic scalar. + // See more comments below. + quote!(__S) + } else { + quote!(#juniper_crate_name::DefaultScalarValue) + } + }); + + let description = self + .description + .as_ref() + .map(|description| quote!( .description(#description) )); + + let meta_types = self.fields.iter().map(|field| { + let var_ty = &field._type; + + quote! { + registry.get_type::<&#var_ty>(&(())), + } + }); + + let all_variants_different = { + let mut all_types: Vec<_> = self.fields.iter().map(|field| &field._type).collect(); + let before = all_types.len(); + all_types.dedup(); + before == all_types.len() + }; + + let matcher_variants = self + .fields + .iter() + .map(|field| { + let variant_ident = quote::format_ident!("{}", field.name); + let var_ty = &field._type; + + quote!( + Self::#variant_ident(ref x) => <#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), + ) + }); + + let concrete_type_resolver = quote!( + match self { + #( #matcher_variants )* + } + ); + + let matcher_expr: Vec<_> = self + .fields + .iter() + .map(|field| { + let variant_ident = quote::format_ident!("{}", field.name); + + quote!( + match self { Self::#variant_ident(ref val) => Some(val), _ => None, } + ) + }) + .collect(); + + let resolve_into_type = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| { + let var_ty = &field._type; + + quote! { + if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { + return executor.resolve(&(), &{ #expr }); + } + } + }); + + let resolve_into_type_async = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| { + let var_ty = &field._type; + + quote! { + if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { + let f = async move { + executor.resolve_async(&(), &{ #expr }).await + }; + use futures::future; + return future::FutureExt::boxed(f); + } + } + }); + + let mut generics = self.generics.clone(); + + if self.scalar.is_none() && self.generic_scalar { + // No custom scalar specified, but always generic specified. + // Therefore we inject the generic scalar. + + generics.params.push(parse_quote!(__S)); + + let where_clause = generics.where_clause.get_or_insert(parse_quote!(where)); + // Insert ScalarValue constraint. + where_clause + .predicates + .push(parse_quote!(__S: #juniper_crate_name::ScalarValue)); + } + + let (impl_generics, _, where_clause) = generics.split_for_impl(); + + let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); + where_async + .predicates + .push(parse_quote!( #scalar: Send + Sync )); + where_async.predicates.push(parse_quote!(Self: Send + Sync)); + + let async_type_impl = quote!( + impl#impl_generics #juniper_crate_name::GraphQLTypeAsync<#scalar> for #ty + #where_async + { + fn resolve_into_type_async<'b>( + &'b self, + _info: &'b Self::TypeInfo, + type_name: &str, + _: Option<&'b [#juniper_crate_name::Selection<'b, #scalar>]>, + executor: &'b #juniper_crate_name::Executor<'b, 'b, Self::Context, #scalar> + ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>> { + let context = &executor.context(); + + #( #resolve_into_type_async )* + + panic!("Concrete type not handled by instance resolvers on {}", #name); + } + } + ); + + let mut type_impl = quote! { + impl #impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn name(_ : &Self::TypeInfo) -> Option<&str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut #juniper_crate_name::Registry<'r, #scalar> + ) -> #juniper_crate_name::meta::MetaType<'r, #scalar> + where + #scalar: 'r, + { + let types = &[ + #( #meta_types )* + ]; + registry.build_union_type::<#ty>( + info, types + ) + #description + .into_meta() + } + + #[allow(unused_variables)] + fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String { + #concrete_type_resolver + } + + fn resolve_into_type( + &self, + _info: &Self::TypeInfo, + type_name: &str, + _: Option<&[#juniper_crate_name::Selection<#scalar>]>, + executor: &#juniper_crate_name::Executor, + ) -> #juniper_crate_name::ExecutionResult<#scalar> { + let context = &executor.context(); + + #( #resolve_into_type )* + + panic!("Concrete type not handled by instance resolvers on {}", #name); + } + } + }; + + if all_variants_different { + let iter = self.fields.iter().map(|field| { + let variant_ty = &field._type; + let variant_ident = quote::format_ident!("{}", field.name); + quote!( + impl std::convert::From<#variant_ty> for #ty { + fn from(val: #variant_ty) -> Self { + Self::#variant_ident(val) + } + } + ) + }); + + type_impl.extend(iter) + } + + if !self.no_async { + type_impl.extend(async_type_impl) + } + + type_impl + } } #[cfg(test)] From 91ae2e2d9f91a6191657d0f7443c9c94b48f083a Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:13:13 +0200 Subject: [PATCH 02/11] Updated PR link in CHNAGELOG --- juniper/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 67563ede7..c4da0799d 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -25,7 +25,7 @@ See [#569](https://github.com/graphql-rust/juniper/pull/569). - GraphQLUnion derive support ("#[derive(GraphqQLUnion)]") - implements GraphQLAsyncType -See [#](https://github.com/graphql-rust/juniper/pull/). +See [#618](https://github.com/graphql-rust/juniper/pull/618). ## Breaking Changes From 3927b1e3d7cf424c73e4fd3b2f5d11acd5b3f15d Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:14:09 +0200 Subject: [PATCH 03/11] Disabled documentation on enumeration fields --- juniper_codegen/src/derive_union.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index 6a03c271a..a85c4ab38 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -67,11 +67,15 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre _ => panic!("#[derive(GraphlQLObject)] all fields of the enum must be unnamed"), }; + if field_attrs.description.is_some() { + panic!("#[derive(GraphQLUnion)] does not allow documentation of fields"); + } + Some(util::GraphQLTypeDefinitionField { name, _type, args: Vec::new(), - description: field_attrs.description, + description: None, deprecation: field_attrs.deprecation, resolver_code, is_type_inferred: true, From 50b25cf925def204ea41e4d01e0f5255397a6ce6 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:15:28 +0200 Subject: [PATCH 04/11] Disabled skip on fields --- juniper_codegen/src/derive_union.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index a85c4ab38..fd6ca0d57 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -38,7 +38,7 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre if field_attrs.skip { - None + panic!("#[derive(GraphQLUnion)] does not support #[graphql(skip)] on fields"); } else { let field_name = field.ident; let name = field_attrs From 4da7c8d025b876bb6165f603898bc4b90c32eec2 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:16:32 +0200 Subject: [PATCH 05/11] Changed implementation for std::convert::Into since skip is not possible --- juniper_codegen/src/derive_union.rs | 21 +++++++++++++++- juniper_codegen/src/util/mod.rs | 37 +++++++++++------------------ 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index fd6ca0d57..6bf2cda75 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -84,13 +84,32 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre } }); + let fields = fields.collect::>(); + + // NOTICE: This is not an optimal implementation. It is possible + // to bypass this check by using a full qualified path instead + // (crate::Test vs Test). Since this requirement is mandatory, the + // `std::convert::Into` implementation is used to enforce this + // requirement. However, due to the bad error message this + // implementation should stay and provide guidance. + let all_variants_different = { + let mut all_types: Vec<_> = fields.iter().map(|field| &field._type).collect(); + let before = all_types.len(); + all_types.dedup(); + before == all_types.len() + }; + + if !all_variants_different { + panic!("#[derive(GraphQLUnion)] each variant must have a different type"); + } + let definition = util::GraphQLTypeDefiniton { name, _type: syn::parse_str(&ast.ident.to_string()).unwrap(), context: attrs.context, scalar: attrs.scalar, description: attrs.description, - fields: fields.collect(), + fields, generics: ast.generics, interfaces: None, include_type_generics: true, diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 593517b92..30a08bf8c 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1294,13 +1294,6 @@ impl GraphQLTypeDefiniton { } }); - let all_variants_different = { - let mut all_types: Vec<_> = self.fields.iter().map(|field| &field._type).collect(); - let before = all_types.len(); - all_types.dedup(); - before == all_types.len() - }; - let matcher_variants = self .fields .iter() @@ -1398,7 +1391,21 @@ impl GraphQLTypeDefiniton { } ); + let convesion_impls = self.fields.iter().map(|field| { + let variant_ty = &field._type; + let variant_ident = quote::format_ident!("{}", field.name); + quote!( + impl std::convert::From<#variant_ty> for #ty { + fn from(val: #variant_ty) -> Self { + Self::#variant_ident(val) + } + } + ) + }); + let mut type_impl = quote! { + #( #convesion_impls )* + impl #impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #where_clause { type Context = #context; @@ -1446,22 +1453,6 @@ impl GraphQLTypeDefiniton { } }; - if all_variants_different { - let iter = self.fields.iter().map(|field| { - let variant_ty = &field._type; - let variant_ident = quote::format_ident!("{}", field.name); - quote!( - impl std::convert::From<#variant_ty> for #ty { - fn from(val: #variant_ty) -> Self { - Self::#variant_ident(val) - } - } - ) - }); - - type_impl.extend(iter) - } - if !self.no_async { type_impl.extend(async_type_impl) } From 645fa9508f0cb59f889877089e022323d7884d07 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:19:47 +0200 Subject: [PATCH 06/11] Added documentation for GraphQLUnion --- docs/book/content/types/unions.md | 42 +++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 198474236..8dd9a3496 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -3,10 +3,11 @@ From a server's point of view, GraphQL unions are similar to interfaces: the only exception is that they don't contain fields on their own. -In Juniper, the `graphql_union!` has identical syntax to the [interface -macro](interfaces.md), but does not support defining fields. Therefore, the same -considerations about using traits, placeholder types, or enums still apply to -unions. +In Juniper, the `graphql_union!` has identical syntax to the +[interface macro](interfaces.md), but does not support defining +fields. Therefore, the same considerations about using traits, +placeholder types, or enums still apply to unions. For simple +situations, Juniper provides `#[derive(GraphQLUnion)]` for enums. If we look at the same examples as in the interfaces chapter, we see the similarities and the tradeoffs: @@ -154,7 +155,7 @@ impl GraphQLUnion for Character { # fn main() {} ``` -## Enums +## Enums (Impl) ```rust #[derive(juniper::GraphQLObject)] @@ -187,3 +188,34 @@ impl Character { # fn main() {} ``` + +## Enums (Derive) + +This example is similar to `Enums (Impl)`. To successfully use the +derive macro, ensure that each variant of the enum has a different +type. Since each variant is different, the device macro provides +`std::convert::Into` converter for each _used_ variant. Fields +which are skipped by using `#[graphql(skip)]` do not have a +`std::convert::Into` converter. + +```rust +#[derive(juniper::GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(juniper::GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(GraphQLUnion)] +enum Character { + Human(Human), + Droid(Droid), +} + +# fn main() {} +``` From 681b8a348ea234f9e11ad23d4ef7cc5aeb6b245d Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:20:01 +0200 Subject: [PATCH 07/11] Added tests for GraphQLUnion --- .../juniper_tests/src/codegen/derive_union.rs | 182 +++++++++++++++++- 1 file changed, 179 insertions(+), 3 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs index 7bae5dd6a..153857bf2 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_union.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -1,4 +1,14 @@ -// Default Test +// Test for union's derive macro + +#[cfg(test)] +use fnv::FnvHashMap; + +#[cfg(test)] +use juniper::{ + self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, + Value, Variables, +}; + #[derive(juniper::GraphQLObject)] pub struct Human { id: String, @@ -12,13 +22,16 @@ pub struct Droid { } #[derive(juniper::GraphQLUnion)] +#[graphql(description = "A Collection of things")] pub enum Character { One(Human), Two(Droid), } // Context Test -pub struct CustomContext; +pub struct CustomContext { + is_left: bool, +} impl juniper::Context for CustomContext {} @@ -36,6 +49,7 @@ pub struct DroidContext { primary_function: String, } +/// A Collection of things #[derive(juniper::GraphQLUnion)] #[graphql(Context = CustomContext)] pub enum CharacterContext { @@ -77,16 +91,178 @@ impl DroidCompat { } } -// NOTICE: this can not compile +// NOTICE: this can not compile due to generic implementation of GraphQLType<__S> // #[derive(juniper::GraphQLUnion)] // pub enum CharacterCompatFail { // One(HumanCompat), // Two(DroidCompat), // } +/// A Collection of things #[derive(juniper::GraphQLUnion)] #[graphql(Scalar = juniper::DefaultScalarValue)] pub enum CharacterCompat { One(HumanCompat), Two(DroidCompat), } + +pub struct Query; + +#[juniper::graphql_object( + Context = CustomContext, +)] +impl Query { + fn context(&self, ctx: &CustomContext) -> CharacterContext { + if ctx.is_left { + HumanContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + } + .into() + } else { + DroidContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + } + .into() + } + } +} + +#[tokio::test] +async fn test_derived_union_doc_macro() { + assert_eq!( + >::name(&()), + Some("Character") + ); + + let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); + let meta = Character::meta(&(), &mut registry); + + assert_eq!(meta.name(), Some("Character")); + assert_eq!( + meta.description(), + Some(&"A Collection of things".to_string()) + ); +} + +#[tokio::test] +async fn test_derived_union_doc_string() { + assert_eq!( + >::name(&()), + Some("CharacterContext") + ); + + let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); + let meta = CharacterContext::meta(&(), &mut registry); + + assert_eq!(meta.name(), Some("CharacterContext")); + assert_eq!( + meta.description(), + Some(&"A Collection of things".to_string()) + ); +} + +#[tokio::test] +async fn test_derived_union_left() { + let doc = r#" + { + context { + ... on HumanContext { + humanId: id + homePlanet + } + ... on DroidContext { + droidId: id + primaryFunction + } + } + }"#; + + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + assert_eq!( + execute( + doc, + None, + &schema, + &Variables::new(), + &CustomContext { is_left: true } + ) + .await, + Ok(( + Value::object( + vec![( + "context", + Value::object( + vec![ + ("humanId", Value::scalar("human-32".to_string())), + ("homePlanet", Value::scalar("earth".to_string())), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect() + ), + vec![] + )) + ); +} + +#[tokio::test] +async fn test_derived_union_right() { + let doc = r#" + { + context { + ... on HumanContext { + humanId: id + homePlanet + } + ... on DroidContext { + droidId: id + primaryFunction + } + } + }"#; + + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + assert_eq!( + execute( + doc, + None, + &schema, + &Variables::new(), + &CustomContext { is_left: false } + ) + .await, + Ok(( + Value::object( + vec![( + "context", + Value::object( + vec![ + ("droidId", Value::scalar("droid-99".to_string())), + ("primaryFunction", Value::scalar("run".to_string())), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect() + ), + vec![] + )) + ); +} From f6cd72decc8886b29ec814eec0b86a2a35c934e3 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 18:21:45 +0200 Subject: [PATCH 08/11] Fixed typos in error messages (as suggested by review) --- juniper_codegen/src/derive_union.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index 6bf2cda75..d67564c8c 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -8,7 +8,7 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre let enum_fields = match ast.data { Data::Enum(data) => data.variants, _ => { - panic!("#[derive(GraphlQLUnion)] can only be applied to enums"); + panic!("#[derive(GraphQLUnion)] can only be applied to enums"); } }; @@ -21,7 +21,7 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre }; if !attrs.interfaces.is_empty() { - panic!("#[derive(GraphlQLUnion)] does not support interfaces"); + panic!("#[derive(GraphQLUnion)] does not support interfaces"); } let ident = &ast.ident; @@ -59,12 +59,12 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre }; if iter.next().is_some() { - panic!("#[derive(GraphlQLUnion)] all members must be unnamed with a single element e.g. Some(T)"); + panic!("#[derive(GraphQLUnion)] all members must be unnamed with a single element e.g. Some(T)"); } first.ty.clone() } - _ => panic!("#[derive(GraphlQLObject)] all fields of the enum must be unnamed"), + _ => panic!("#[derive(GraphQLUnion)] all fields of the enum must be unnamed"), }; if field_attrs.description.is_some() { From 3295bc5633f9c3d274422a9808d35c8bfd4da5f5 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Mon, 13 Apr 2020 19:15:13 +0200 Subject: [PATCH 09/11] Fixed failing documentation example --- docs/book/content/types/unions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 8dd9a3496..f66eababa 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -211,7 +211,7 @@ struct Droid { primary_function: String, } -#[derive(GraphQLUnion)] +#[derive(juniper::GraphQLUnion)] enum Character { Human(Human), Droid(Droid), From c308bd70bed1264c2d6157d0ec33265c5b96a148 Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Tue, 14 Apr 2020 14:07:45 +0200 Subject: [PATCH 10/11] Utilized `resolver_code` in `util::GraphQLTypeDefinitionField`. Simplifies code and provides the idea of using `util::GraphQLTypeDefinitionField` for different types than objects. --- juniper_codegen/src/derive_union.rs | 6 +++--- juniper_codegen/src/util/mod.rs | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index d67564c8c..83be43fc2 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -40,14 +40,14 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre if field_attrs.skip { panic!("#[derive(GraphQLUnion)] does not support #[graphql(skip)] on fields"); } else { - let field_name = field.ident; + let variant_name = field.ident; let name = field_attrs .name .clone() - .unwrap_or_else(|| util::to_camel_case(&field_name.to_string())); + .unwrap_or_else(|| util::to_camel_case(&variant_name.to_string())); let resolver_code = quote!( - #ident . #field_name + #ident :: #variant_name ); let _type = match field.fields { diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 30a08bf8c..b55a3ceae 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1298,11 +1298,11 @@ impl GraphQLTypeDefiniton { .fields .iter() .map(|field| { - let variant_ident = quote::format_ident!("{}", field.name); let var_ty = &field._type; + let resolver_code = &field.resolver_code; quote!( - Self::#variant_ident(ref x) => <#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), + #resolver_code(ref x) => <#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), ) }); @@ -1316,10 +1316,10 @@ impl GraphQLTypeDefiniton { .fields .iter() .map(|field| { - let variant_ident = quote::format_ident!("{}", field.name); + let resolver_code = &field.resolver_code; quote!( - match self { Self::#variant_ident(ref val) => Some(val), _ => None, } + match self { #resolver_code(ref val) => Some(val), _ => None, } ) }) .collect(); @@ -1393,11 +1393,12 @@ impl GraphQLTypeDefiniton { let convesion_impls = self.fields.iter().map(|field| { let variant_ty = &field._type; - let variant_ident = quote::format_ident!("{}", field.name); + let resolver_code = &field.resolver_code; + quote!( impl std::convert::From<#variant_ty> for #ty { fn from(val: #variant_ty) -> Self { - Self::#variant_ident(val) + #resolver_code(val) } } ) From 872b3da537d0c2614ab0e72aa4f393699b347c4a Mon Sep 17 00:00:00 2001 From: Jonas Meurer Date: Tue, 14 Apr 2020 14:09:48 +0200 Subject: [PATCH 11/11] Removed wrong statement about skip annotation in docs. --- docs/book/content/types/unions.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index f66eababa..a2793a626 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -194,9 +194,7 @@ impl Character { This example is similar to `Enums (Impl)`. To successfully use the derive macro, ensure that each variant of the enum has a different type. Since each variant is different, the device macro provides -`std::convert::Into` converter for each _used_ variant. Fields -which are skipped by using `#[graphql(skip)]` do not have a -`std::convert::Into` converter. +`std::convert::Into` converter for each variant. ```rust #[derive(juniper::GraphQLObject)]