Improve union
proc macro
* Rename to graphql_union * Implement full-featured parsing and code generation
This commit is contained in:
parent
6861951a1e
commit
9ce3d04007
12 changed files with 298 additions and 88 deletions
44
integration_tests/juniper_tests/src/codegen/impl_union.rs
Normal file
44
integration_tests/juniper_tests/src/codegen/impl_union.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -234,7 +234,7 @@ impl GraphQLType<DefaultScalarValue> for User
|
|||
```
|
||||
|
||||
*/
|
||||
pub trait GraphQLType<S>: Sized
|
||||
pub trait GraphQLType<S = DefaultScalarValue>: Sized
|
||||
where
|
||||
S: ScalarValue,
|
||||
{
|
||||
|
|
|
@ -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 )*
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
pub mod parse_impl;
|
||||
|
||||
use quote::quote;
|
||||
use std::collections::HashMap;
|
||||
use syn::{
|
177
juniper_codegen/src/util/parse_impl.rs
Normal file
177
juniper_codegen/src/util/parse_impl.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue