Derive macro for tagged enums (GraphQLUnion) (#618)

* Implemented device macro for GraphQLUnion's

* Updated PR link in CHNAGELOG

* Disabled documentation on enumeration fields

* Disabled skip on fields

* Changed implementation for std::convert::Into since skip is not possible

* Added documentation for GraphQLUnion

* Added tests for GraphQLUnion

* Fixed typos in error messages (as suggested by review)

* Fixed failing documentation example

* Utilized `resolver_code` in `util::GraphQLTypeDefinitionField`.

Simplifies code and provides the idea of using
`util::GraphQLTypeDefinitionField` for different types than objects.

* Removed wrong statement about skip annotation in docs.

Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
This commit is contained in:
Jonas Meurer 2020-04-15 09:58:45 +02:00 committed by GitHub
parent 47f7ffaa5b
commit a05f4e55c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 645 additions and 6 deletions

View file

@ -3,10 +3,11 @@
From a server's point of view, GraphQL unions are similar to interfaces: the 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. only exception is that they don't contain fields on their own.
In Juniper, the `graphql_union!` has identical syntax to the [interface In Juniper, the `graphql_union!` has identical syntax to the
macro](interfaces.md), but does not support defining fields. Therefore, the same [interface macro](interfaces.md), but does not support defining
considerations about using traits, placeholder types, or enums still apply to fields. Therefore, the same considerations about using traits,
unions. 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 If we look at the same examples as in the interfaces chapter, we see the
similarities and the tradeoffs: similarities and the tradeoffs:
@ -154,7 +155,7 @@ impl GraphQLUnion for Character {
# fn main() {} # fn main() {}
``` ```
## Enums ## Enums (Impl)
```rust ```rust
#[derive(juniper::GraphQLObject)] #[derive(juniper::GraphQLObject)]
@ -187,3 +188,32 @@ impl Character {
# fn main() {} # 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<T>` 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() {}
```

View file

@ -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!(
<Character as GraphQLType<DefaultScalarValue>>::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!(
<CharacterContext as GraphQLType<DefaultScalarValue>>::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::<CustomContext>::new(),
EmptySubscription::<CustomContext>::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::<CustomContext>::new(),
EmptySubscription::<CustomContext>::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![]
))
);
}

View file

@ -2,5 +2,6 @@ mod derive_enum;
mod derive_input_object; mod derive_input_object;
mod derive_object; mod derive_object;
mod derive_object_with_raw_idents; mod derive_object_with_raw_idents;
mod derive_union;
mod impl_union; mod impl_union;
mod scalar_value_transparent; mod scalar_value_transparent;

View file

@ -22,6 +22,11 @@ See [#419](https://github.com/graphql-rust/juniper/pull/419).
See [#569](https://github.com/graphql-rust/juniper/pull/569). 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 ## Breaking Changes
- `juniper::graphiql` has moved to `juniper::http::graphiql` - `juniper::graphiql` has moved to `juniper::http::graphiql`

View file

@ -116,7 +116,7 @@ extern crate bson;
// functionality automatically. // functionality automatically.
pub use juniper_codegen::{ pub use juniper_codegen::{
graphql_object, graphql_subscription, graphql_union, GraphQLEnum, GraphQLInputObject, graphql_object, graphql_subscription, graphql_union, GraphQLEnum, GraphQLInputObject,
GraphQLObject, GraphQLScalarValue, GraphQLObject, GraphQLScalarValue, GraphQLUnion,
}; };
// Internal macros are not exported, // Internal macros are not exported,
// but declared at the root to make them easier to use. // but declared at the root to make them easier to use.

View file

@ -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::<Vec<_>>();
// 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<T>` 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)
}

View file

@ -15,6 +15,7 @@ mod derive_enum;
mod derive_input_object; mod derive_input_object;
mod derive_object; mod derive_object;
mod derive_scalar_value; mod derive_scalar_value;
mod derive_union;
mod impl_object; mod impl_object;
mod impl_union; mod impl_union;
@ -63,6 +64,13 @@ pub fn derive_object_internal(input: TokenStream) -> TokenStream {
let gen = derive_object::build_derive_object(ast, true); let gen = derive_object::build_derive_object(ast, true);
gen.into() gen.into()
} }
#[proc_macro_derive(GraphQLUnion, attributes(graphql))]
pub fn derive_union(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_union::build_derive_union(ast, false);
gen.into()
}
/// This custom derive macro implements the #[derive(GraphQLScalarValue)] /// This custom derive macro implements the #[derive(GraphQLScalarValue)]
/// derive. /// derive.
/// ///

View file

@ -1255,6 +1255,211 @@ impl GraphQLTypeDefiniton {
#subscription_implementation #subscription_implementation
) )
} }
pub fn into_union_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(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<Self::Context, #scalar>,
) -> #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)] #[cfg(test)]