Improve union proc macro

* Rename to graphql_union
* Implement full-featured parsing and code generation
This commit is contained in:
Christoph Herzog 2019-11-16 02:20:36 +01:00
parent 6861951a1e
commit 9ce3d04007
12 changed files with 298 additions and 88 deletions

View file

@ -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(),
}
}
}

View file

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

View file

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

View file

@ -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(),

View file

@ -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,
};

View file

@ -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<T> WithGenerics<T> {
fn resolve(&self) {
match self {
@ -78,7 +78,7 @@ impl<T> WithGenerics<T> {
}
}
#[crate::union_internal(description = "A description")]
#[crate::graphql_union_internal(description = "A description")]
impl DescriptionFirst {
fn resolve(&self) {
match self {

View file

@ -234,7 +234,7 @@ impl GraphQLType<DefaultScalarValue> for User
```
*/
pub trait GraphQLType<S>: Sized
pub trait GraphQLType<S = DefaultScalarValue>: Sized
where
S: ScalarValue,
{

View file

@ -17,30 +17,11 @@ struct ResolveBody {
impl syn::parse::Parse for ResolveBody {
fn parse(input: syn::parse::ParseStream) -> Result<Self, syn::parse::Error> {
input.parse::<syn::token::Fn>()?;
let ident = input.parse::<syn::Ident>()?;
if ident != "resolve" {
return Err(input.error("Expected method named 'resolve'"));
}
let args;
syn::parenthesized!(args in input);
args.parse::<syn::token::And>()?;
args.parse::<syn::token::SelfValue>()?;
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::<syn::token::Match>()?;
body.parse::<syn::token::SelfValue>()?;
input.parse::<syn::token::Match>()?;
input.parse::<syn::token::SelfValue>()?;
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::<syn::token::Comma>().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<TokenStream, MacroError> {
// We are re-using the object attributes since they are almost the same.
let attrs = syn::parse::<util::ObjectAttributes>(attrs)?;
let _impl = util::parse_impl::ImplBlock::parse(attrs, body);
let item = syn::parse::<syn::ItemImpl>(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::<ResolveBody>(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::<ResolveBody>(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<Self::Context, #scalar>,
) -> #juniper::ExecutionResult<#scalar> {
_: Option<&[#crate_name::Selection<#scalar>]>,
executor: &#crate_name::Executor<Self::Context, #scalar>,
) -> #crate_name::ExecutionResult<#scalar> {
let context = &executor.context();
#( #resolve_args )*
#( #resolve_into_type )*

View file

@ -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),

View file

@ -1,3 +1,5 @@
pub mod parse_impl;
use quote::quote;
use std::collections::HashMap;
use syn::{

View file

@ -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<syn::Type>,
pub type_ident: syn::Ident,
pub generics: syn::Generics,
// _impl: syn::ItemImpl,
pub methods: Vec<syn::ImplItemMethod>,
pub description: Option<String>,
}
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<proc_macro2::TokenStream> {
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::<util::ObjectAttributes>(attr_tokens) {
Ok(attrs) => attrs,
Err(e) => {
panic!("Invalid attributes:\n{}", e);
}
};
let mut _impl = match syn::parse::<syn::ItemImpl>(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::<Vec<_>>()
.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,
}
}
}