diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 19847423..a2793a62 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,32 @@ 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 variant. + +```rust +#[derive(juniper::GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(juniper::GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(juniper::GraphQLUnion)] +enum Character { + Human(Human), + Droid(Droid), +} + +# fn main() {} +``` 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 00000000..153857bf --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -0,0 +1,268 @@ +// 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, + home_planet: String, +} + +#[derive(juniper::GraphQLObject)] +pub struct Droid { + id: String, + primary_function: String, +} + +#[derive(juniper::GraphQLUnion)] +#[graphql(description = "A Collection of things")] +pub enum Character { + One(Human), + Two(Droid), +} + +// Context Test +pub struct CustomContext { + is_left: bool, +} + +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, +} + +/// A Collection of things +#[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 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![] + )) + ); +} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 1f5a2bb0..8c3baeb1 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 80a88f3f..c4da0799 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -22,6 +22,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 [#618](https://github.com/graphql-rust/juniper/pull/618). + ## Breaking Changes - `juniper::graphiql` has moved to `juniper::http::graphiql` diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 4f08a099..30e5edc4 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 00000000..83be43fc --- /dev/null +++ b/juniper_codegen/src/derive_union.rs @@ -0,0 +1,122 @@ +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(GraphQLUnion)] 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(GraphQLUnion)] 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 { + panic!("#[derive(GraphQLUnion)] does not support #[graphql(skip)] on fields"); + } else { + let variant_name = field.ident; + let name = field_attrs + .name + .clone() + .unwrap_or_else(|| util::to_camel_case(&variant_name.to_string())); + + let resolver_code = quote!( + #ident :: #variant_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(GraphQLUnion)] all members must be unnamed with a single element e.g. Some(T)"); + } + + first.ty.clone() + } + _ => panic!("#[derive(GraphQLUnion)] 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: None, + deprecation: field_attrs.deprecation, + resolver_code, + is_type_inferred: true, + is_async: false, + }) + } + }); + + 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, + 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 562e4a2f..923519a4 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 b9969e20..b55a3cea 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1255,6 +1255,211 @@ 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 matcher_variants = self + .fields + .iter() + .map(|field| { + let var_ty = &field._type; + let resolver_code = &field.resolver_code; + + quote!( + #resolver_code(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 resolver_code = &field.resolver_code; + + quote!( + match self { #resolver_code(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 convesion_impls = self.fields.iter().map(|field| { + let variant_ty = &field._type; + let resolver_code = &field.resolver_code; + + quote!( + impl std::convert::From<#variant_ty> for #ty { + fn from(val: #variant_ty) -> Self { + #resolver_code(val) + } + } + ) + }); + + let mut type_impl = quote! { + #( #convesion_impls )* + + 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 !self.no_async { + type_impl.extend(async_type_impl) + } + + type_impl + } } #[cfg(test)]