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
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<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_object;
mod derive_object_with_raw_idents;
mod derive_union;
mod impl_union;
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).
- 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`

View file

@ -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.

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_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::<syn::DeriveInput>(input).unwrap();
let gen = derive_union::build_derive_union(ast, false);
gen.into()
}
/// This custom derive macro implements the #[derive(GraphQLScalarValue)]
/// derive.
///

View file

@ -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::<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)]