diff --git a/integration_tests/juniper_tests/src/codegen/impl_union.rs b/integration_tests/juniper_tests/src/codegen/impl_union.rs new file mode 100644 index 00000000..5ed28a3f --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/impl_union.rs @@ -0,0 +1,44 @@ +// Trait. + +#[derive(juniper::GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(juniper::GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } +} + +impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } +} + +impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } +} + +#[juniper::graphql_union] +impl<'a> GraphQLUnion for &'a dyn Character { + fn resolve(&self) { + match self { + Human => self.as_human(), + Droid => self.as_droid(), + } + } +} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index 4fcc5351..084e022e 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -4,5 +4,5 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_object_with_raw_idents; +mod impl_union; mod scalar_value_transparent; -mod unions; diff --git a/integration_tests/juniper_tests/src/codegen/unions.rs b/integration_tests/juniper_tests/src/codegen/unions.rs deleted file mode 100644 index 8b137891..00000000 --- a/integration_tests/juniper_tests/src/codegen/unions.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index e697cce7..c61c9734 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -6,8 +6,12 @@ ## Breaking Changes -- Remove ScalarRefValue trait -- Change return type of GraphQLType::resolve to `ExecutionResult` +- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro + +- ScalarRefValue trait removed + Trait was not required. + +- Changed return type of GraphQLType::resolve to `ExecutionResult` This was done to unify the return type of all resolver methods The previous `Value` return type was just an internal artifact of error handling. diff --git a/juniper/src/executor_tests/interfaces_unions.rs b/juniper/src/executor_tests/interfaces_unions.rs index 324befd2..e7cd87cf 100644 --- a/juniper/src/executor_tests/interfaces_unions.rs +++ b/juniper/src/executor_tests/interfaces_unions.rs @@ -166,17 +166,8 @@ mod union { } } - /* - graphql_union!(<'a> &'a dyn Pet: () as "Pet" |&self| { - instance_resolvers: |&_| { - &Dog => self.as_dog(), - &Cat => self.as_cat(), - } - }); - */ - - #[crate::union_internal] - impl<'a> &'a dyn Pet { + #[crate::graphql_union_internal] + impl<'a> GraphQLUnion for &'a dyn Pet { fn resolve(&self) { match self { Dog => self.as_dog(), diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 68a132bb..9f06479b 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -110,13 +110,14 @@ extern crate uuid; // This allows users to just depend on juniper and get the derive // functionality automatically. pub use juniper_codegen::{ - object, union, GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalarValue, ScalarValue, + graphql_union, object, GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalarValue, + ScalarValue, }; // Internal macros are not exported, // but declared at the root to make them easier to use. #[allow(unused_imports)] use juniper_codegen::{ - object_internal, union_internal, GraphQLEnumInternal, GraphQLInputObjectInternal, + graphql_union_internal, object_internal, GraphQLEnumInternal, GraphQLInputObjectInternal, GraphQLScalarValueInternal, }; diff --git a/juniper/src/macros/tests/union.rs b/juniper/src/macros/tests/union.rs index f6b6e622..6e9f2972 100644 --- a/juniper/src/macros/tests/union.rs +++ b/juniper/src/macros/tests/union.rs @@ -45,7 +45,7 @@ impl Concrete { } } -#[crate::union_internal(name = "ACustomNamedUnion")] +#[crate::graphql_union_internal(name = "ACustomNamedUnion")] impl CustomName { fn resolve(&self) { match self { @@ -56,7 +56,7 @@ impl CustomName { } } -#[crate::union_internal] +#[crate::graphql_union_internal] impl<'a> WithLifetime<'a> { fn resolve(&self) { match self { @@ -67,7 +67,7 @@ impl<'a> WithLifetime<'a> { } } -#[crate::union_internal] +#[crate::graphql_union_internal] impl WithGenerics { fn resolve(&self) { match self { @@ -78,7 +78,7 @@ impl WithGenerics { } } -#[crate::union_internal(description = "A description")] +#[crate::graphql_union_internal(description = "A description")] impl DescriptionFirst { fn resolve(&self) { match self { diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index bc4d0003..ea56101f 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -234,7 +234,7 @@ impl GraphQLType for User ``` */ -pub trait GraphQLType: Sized +pub trait GraphQLType: Sized where S: ScalarValue, { diff --git a/juniper_codegen/src/impl_union.rs b/juniper_codegen/src/impl_union.rs index a448e345..6c42376e 100644 --- a/juniper_codegen/src/impl_union.rs +++ b/juniper_codegen/src/impl_union.rs @@ -17,30 +17,11 @@ struct ResolveBody { impl syn::parse::Parse for ResolveBody { fn parse(input: syn::parse::ParseStream) -> Result { - input.parse::()?; - let ident = input.parse::()?; - if ident != "resolve" { - return Err(input.error("Expected method named 'resolve'")); - } - - let args; - syn::parenthesized!(args in input); - args.parse::()?; - args.parse::()?; - if !args.is_empty() { - return Err( - input.error("Unexpected extra tokens: only one '&self' parameter is allowed") - ); - } - - let body; - syn::braced!( body in input ); - - body.parse::()?; - body.parse::()?; + input.parse::()?; + input.parse::()?; let match_body; - syn::braced!( match_body in body ); + syn::braced!( match_body in input ); let mut variants = Vec::new(); while !match_body.is_empty() { @@ -54,7 +35,7 @@ impl syn::parse::Parse for ResolveBody { match_body.parse::().ok(); } - if !body.is_empty() { + if !input.is_empty() { return Err(input.error("Unexpected input")); } @@ -67,36 +48,51 @@ pub fn impl_union( attrs: TokenStream, body: TokenStream, ) -> Result { - // We are re-using the object attributes since they are almost the same. - let attrs = syn::parse::(attrs)?; + let _impl = util::parse_impl::ImplBlock::parse(attrs, body); - let item = syn::parse::(body)?; + // Validate trait target name, if present. + if let Some((name, path)) = &_impl.target_trait { + if !(name == "GraphQLUnion" || name == "juniper.GraphQLUnion") { + return Err(MacroError::new( + path.span(), + "Invalid impl target trait: expected 'GraphQLUnion'".to_string(), + )); + } + } - if item.items.len() != 1 { + let type_ident = &_impl.type_ident; + let name = _impl + .attrs + .name + .clone() + .unwrap_or_else(|| type_ident.to_string()); + let crate_name = util::juniper_path(is_internal); + + let scalar = _impl + .attrs + .scalar + .as_ref() + .map(|s| quote!( #s )) + .unwrap_or_else(|| { + quote! { #crate_name::DefaultScalarValue } + }); + + if _impl.methods.len() != 1 { return Err(MacroError::new( - item.span(), + _impl.target_type.span(), "Invalid impl body: expected one method with signature: fn resolve(&self) { ... }" .to_string(), )); } + let method = _impl.methods.first().unwrap(); - let body_item = item.items.first().unwrap(); - let body = quote! { #body_item }; - let variants = syn::parse::(body.into())?.variants; + let resolve_args = _impl.parse_resolve_method(method); - let ty = &item.self_ty; + let stmts = &method.block.stmts; + let body_raw = quote!( #( #stmts )* ); + let body = syn::parse::(body_raw.into())?; - let ty_ident = util::name_of_type(&*ty).ok_or_else(|| { - MacroError::new( - ty.span(), - "Expected a path ending in a simple type identifier".to_string(), - ) - })?; - let name = attrs.name.unwrap_or_else(|| ty_ident.to_string()); - - let juniper = util::juniper_path(is_internal); - - let meta_types = variants.iter().map(|var| { + let meta_types = body.variants.iter().map(|var| { let var_ty = &var.ty; quote! { @@ -104,50 +100,45 @@ pub fn impl_union( } }); - let concrete_type_resolver = variants.iter().map(|var| { + let concrete_type_resolver = body.variants.iter().map(|var| { let var_ty = &var.ty; let resolve = &var.resolver; quote! { if ({#resolve} as std::option::Option<&#var_ty>).is_some() { - return <#var_ty as #juniper::GraphQLType<_>>::name(&()).unwrap().to_string(); + return <#var_ty as #crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(); } } }); - let resolve_into_type = variants.iter().map(|var| { + let resolve_into_type = body.variants.iter().map(|var| { let var_ty = &var.ty; let resolve = &var.resolver; quote! { - if type_name == (<#var_ty as #juniper::GraphQLType<_>>::name(&())).unwrap() { + if type_name == (<#var_ty as #crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { return executor.resolve(&(), &{ #resolve }); } } }); - let scalar = attrs - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - quote! { #juniper::DefaultScalarValue } - }); - - let generics = item.generics.clone(); + let generics = _impl.generics; let (impl_generics, _, where_clause) = generics.split_for_impl(); - let description = match attrs.description.as_ref() { + let description = match _impl.description.as_ref() { Some(value) => quote!( .description( #value ) ), None => quote!(), }; - let context = attrs + let context = _impl + .attrs .context .map(|c| quote! { #c }) .unwrap_or_else(|| quote! { () }); + let ty = _impl.target_type; + let output = quote! { - impl #impl_generics #juniper::GraphQLType<#scalar> for #ty #where_clause + impl #impl_generics #crate_name::GraphQLType<#scalar> for #ty #where_clause { type Context = #context; type TypeInfo = (); @@ -158,8 +149,8 @@ pub fn impl_union( fn meta<'r>( info: &Self::TypeInfo, - registry: &mut #juniper::Registry<'r, #scalar> - ) -> #juniper::meta::MetaType<'r, #scalar> + registry: &mut #crate_name::Registry<'r, #scalar> + ) -> #crate_name::meta::MetaType<'r, #scalar> where #scalar: 'r, { @@ -184,10 +175,11 @@ pub fn impl_union( &self, _info: &Self::TypeInfo, type_name: &str, - _: Option<&[#juniper::Selection<#scalar>]>, - executor: &#juniper::Executor, - ) -> #juniper::ExecutionResult<#scalar> { + _: Option<&[#crate_name::Selection<#scalar>]>, + executor: &#crate_name::Executor, + ) -> #crate_name::ExecutionResult<#scalar> { let context = &executor.context(); + #( #resolve_args )* #( #resolve_into_type )* diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 8be08a51..b2427f7f 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -389,7 +389,7 @@ pub fn object_internal(args: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_attribute] #[proc_macro_error::proc_macro_error] -pub fn union(attrs: TokenStream, body: TokenStream) -> TokenStream { +pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { let output = match impl_union::impl_union(false, attrs, body) { Ok(toks) => toks, Err(err) => proc_macro_error::abort!(err), @@ -400,7 +400,7 @@ pub fn union(attrs: TokenStream, body: TokenStream) -> TokenStream { #[doc(hidden)] #[proc_macro_attribute] #[proc_macro_error::proc_macro_error] -pub fn union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { +pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { let output = match impl_union::impl_union(true, attrs, body) { Ok(toks) => toks, Err(err) => proc_macro_error::abort!(err), diff --git a/juniper_codegen/src/util.rs b/juniper_codegen/src/util/mod.rs similarity index 99% rename from juniper_codegen/src/util.rs rename to juniper_codegen/src/util/mod.rs index 585e791f..97d993c3 100644 --- a/juniper_codegen/src/util.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1,3 +1,5 @@ +pub mod parse_impl; + use quote::quote; use std::collections::HashMap; use syn::{ diff --git a/juniper_codegen/src/util/parse_impl.rs b/juniper_codegen/src/util/parse_impl.rs new file mode 100644 index 00000000..d810fec7 --- /dev/null +++ b/juniper_codegen/src/util/parse_impl.rs @@ -0,0 +1,177 @@ +//! Parse impl blocks. + +use proc_macro::TokenStream; +use quote::quote; + +use crate::util; + +pub struct ImplBlock { + pub attrs: util::ObjectAttributes, + pub target_trait: Option<(String, syn::Path)>, + pub target_type: Box, + pub type_ident: syn::Ident, + pub generics: syn::Generics, + // _impl: syn::ItemImpl, + pub methods: Vec, + pub description: Option, +} + +impl ImplBlock { + /// Parse a 'fn resolve()' metho declaration found in union or interface + /// impl blocks. + /// Returns the variable definitions needed for the resolve body. + pub fn parse_resolve_method( + &self, + method: &syn::ImplItemMethod, + ) -> Vec { + if method.sig.ident != "resolve" { + panic!("Expect a method named 'fn resolve(...)"); + } + + let _type = match &method.sig.output { + syn::ReturnType::Type(_, _) => { + panic!("resolve() method must not have a declared return type"); + } + syn::ReturnType::Default => {} + }; + + let mut arguments = method.sig.inputs.iter(); + + // Verify '&self' argument. + match arguments.next() { + Some(syn::FnArg::Receiver(rec)) => { + if rec.reference.is_none() || rec.mutability.is_some() { + panic!( + "Invalid method receiver {}(self, ...): did you mean '&self'?", + method.sig.ident + ); + } + } + _ => { + panic!("Expected a '&self' argument"); + } + } + + let mut resolve_parts = Vec::new(); + + for arg in arguments { + match arg { + syn::FnArg::Receiver(_) => { + panic!( + "Malformed method signature {}: self receiver must be the first argument", + method.sig.ident + ); + } + syn::FnArg::Typed(captured) => { + let (arg_ident, _is_mut) = match &*captured.pat { + syn::Pat::Ident(ref pat_ident) => { + (&pat_ident.ident, pat_ident.mutability.is_some()) + } + _ => { + panic!("Invalid token for function argument"); + } + }; + let context_type = self.attrs.context.as_ref(); + + // Check for executor arguments. + if util::type_is_identifier_ref(&captured.ty, "Executor") { + resolve_parts.push(quote!(let #arg_ident = executor;)); + } + // Make sure executor is specified as a reference. + else if util::type_is_identifier(&captured.ty, "Executor") { + panic!("Invalid executor argument: to access the Executor, you need to specify the type as a reference.\nDid you mean &Executor?"); + } + // Check for context arg. + else if context_type + .clone() + .map(|ctx| util::type_is_ref_of(&captured.ty, ctx)) + .unwrap_or(false) + { + resolve_parts.push(quote!( let #arg_ident = executor.context(); )); + } + // Make sure the user does not specify the Context + // without a reference. (&Context) + else if context_type + .clone() + .map(|ctx| ctx == &*captured.ty) + .unwrap_or(false) + { + panic!( + "Invalid context argument: to access the context, you need to specify the type as a reference.\nDid you mean &{}?", + quote!(captured.ty), + ); + } else { + panic!("Invalid argument for 'resolve' method: only executor or context are allowed"); + } + } + } + } + + resolve_parts + } + + pub fn parse(attr_tokens: TokenStream, body: TokenStream) -> ImplBlock { + let attrs = match syn::parse::(attr_tokens) { + Ok(attrs) => attrs, + Err(e) => { + panic!("Invalid attributes:\n{}", e); + } + }; + + let mut _impl = match syn::parse::(body) { + Ok(item) => item, + Err(err) => { + panic!("Parsing error:\n{}", err); + } + }; + + let target_trait = match _impl.trait_ { + Some((_, path, _)) => { + let name = path + .segments + .iter() + .map(|segment| segment.ident.to_string()) + .collect::>() + .join("."); + Some((name, path)) + } + None => None, + }; + + let type_ident = if let Some(ident) = util::name_of_type(&*_impl.self_ty) { + ident + } else { + panic!("Could not determine a name for the impl type"); + }; + + let target_type = _impl.self_ty; + + let description = attrs + .description + .clone() + .or(util::get_doc_comment(&_impl.attrs)); + + let mut methods = Vec::new(); + + for item in _impl.items { + match item { + syn::ImplItem::Method(method) => { + methods.push(method); + } + _ => { + panic!("Invalid item for GraphQL Object: only type declarations and methods are allowed"); + } + } + } + + Self { + attrs, + type_ident, + target_trait, + target_type, + generics: _impl.generics, + description, + methods, + } + } +}