juniper/juniper_codegen/src/derive_union.rs
Jonas Meurer a05f4e55c4
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>
2020-04-14 21:58:45 -10:00

122 lines
4 KiB
Rust

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)
}