Codegen: Add custom derive for GraphQLObject

This adds a simple derive for objects that implements GraphQLType for
structs which do not require any manual resolvers.

This could be extended in the future to provide a custom resolver
function via an attribute.

Also adds an integration test.
This commit is contained in:
theduke 2017-08-03 03:49:19 +02:00 committed by theduke
parent 27048ae7cd
commit e835a5e019
5 changed files with 236 additions and 0 deletions

View file

@ -10,6 +10,7 @@ The repository was restructured to a multi crate workspace to enable several new
* New juniper_codegen crate which provides custom derives: * New juniper_codegen crate which provides custom derives:
* `#[derive(GraphQLInputObject)]` * `#[derive(GraphQLInputObject)]`
* `#[derive(GraphQLEnum)]` * `#[derive(GraphQLEnum)]`
* `#[derive(GraphQLObject)]`
## [0.8.1] 2017-06-15 ## [0.8.1] 2017-06-15

View file

@ -0,0 +1,181 @@
use syn;
use syn::*;
use quote::Tokens;
use ::util::*;
#[derive(Default, Debug)]
struct ObjAttrs {
name: Option<String>,
description: Option<String>,
}
impl ObjAttrs {
fn from_input(input: &DeriveInput) -> ObjAttrs {
let mut res = ObjAttrs::default();
// Check attributes for name and description.
if let Some(items) = get_graphl_attr(&input.attrs) {
for item in items {
if let Some(val) = keyed_item_value(item, "name", true) {
res.name = Some(val);
continue;
}
if let Some(val) = keyed_item_value(item, "description", true) {
res.description = Some(val);
continue;
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLObject)]: {:?}",
item));
}
}
res
}
}
#[derive(Default)]
struct ObjFieldAttrs {
name: Option<String>,
description: Option<String>,
deprecation: Option<String>,
}
impl ObjFieldAttrs {
fn from_input(variant: &Field) -> ObjFieldAttrs {
let mut res = ObjFieldAttrs::default();
// Check attributes for name and description.
if let Some(items) = get_graphl_attr(&variant.attrs) {
for item in items {
if let Some(val) = keyed_item_value(item, "name", true) {
res.name = Some(val);
continue;
}
if let Some(val) = keyed_item_value(item, "description", true) {
res.description = Some(val);
continue;
}
if let Some(val) = keyed_item_value(item, "deprecation", true) {
res.deprecation = Some(val);
continue;
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLObject)]: {:?}",
item));
}
}
res
}
}
pub fn impl_object(ast: &syn::DeriveInput) -> Tokens {
let fields = match ast.body {
Body::Struct(ref data) => {
match data {
&VariantData::Struct(ref fields) => fields,
_ => {
panic!("#[derive(GraphQLObject)] may only be used on regular structs with fields");
},
}
},
Body::Enum(_) => {
panic!("#[derive(GraphlQLObject)] may only be applied to structs, not to enums");
},
};
// Parse attributes.
let ident = &ast.ident;
let ident_name = ident.to_string();
let attrs = ObjAttrs::from_input(ast);
let name = attrs.name.unwrap_or(ast.ident.to_string());
let build_description = match attrs.description {
Some(s) => quote!{ builder.description(#s) },
None => quote!{ builder },
};
let mut meta_fields = Vec::<Tokens>::new();
let mut resolvers = Vec::<Tokens>::new();
for field in fields {
let field_ty = &field.ty;
let field_attrs = ObjFieldAttrs::from_input(field);
let field_ident = field.ident.as_ref().unwrap();
let field_ident_name = field_ident.to_string();
// Build value.
let name = match field_attrs.name {
Some(ref name) => {
// Custom name specified.
name.to_string()
},
None => {
// Note: auto camel casing when no custom name specified.
::util::to_camel_case(field_ident.as_ref())
},
};
let build_description = match field_attrs.description {
Some(s) => quote!{ field.description(#s) },
None => quote!{ field },
};
let build_deprecation = match field_attrs.deprecation {
Some(s) => quote!{ field.deprecated(#s) },
None => quote!{ field },
};
let meta_field = quote!{
{
let field = registry.field::<#field_ty>(#name);
let field = #build_description;
let field = #build_deprecation;
field
},
};
meta_fields.push(meta_field);
// Build from_input clause.
let resolver = quote!{
#name => executor.resolve_with_ctx(&self.#field_ident),
};
resolvers.push(resolver);
}
let toks = quote! {
impl ::juniper::GraphQLType for #ident {
type Context = ();
fn name() -> Option<&'static str> {
Some(#name)
}
fn concrete_type_name(&self, context: &Self::Context) -> String {
#name.to_string()
}
fn meta<'r>(registry: &mut ::juniper::Registry<'r>) -> ::juniper::meta::MetaType<'r> {
let fields = &[
#(#meta_fields)*
];
let builder = registry.build_object_type::<#ident>(fields);
let builder = #build_description;
builder.into_meta()
}
fn resolve_field(&self, field_name: &str, args: &::juniper::Arguments, executor: &::juniper::Executor<Self::Context>)
-> ::juniper::ExecutionResult
{
match field_name {
#(#resolvers)*
_ => panic!("Field {} not found on type {}", field_name, #ident_name),
}
}
}
};
toks
}

View file

@ -8,6 +8,7 @@ extern crate quote;
mod util; mod util;
mod enums; mod enums;
mod input_objects; mod input_objects;
mod derive_object;
use proc_macro::TokenStream; use proc_macro::TokenStream;
@ -26,3 +27,11 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream {
let gen = input_objects::impl_input_object(&ast); let gen = input_objects::impl_input_object(&ast);
gen.parse().unwrap() gen.parse().unwrap()
} }
#[proc_macro_derive(GraphQLObject, attributes(graphql))]
pub fn derive_object(input: TokenStream) -> TokenStream {
let s = input.to_string();
let ast = syn::parse_derive_input(&s).unwrap();
let gen = derive_object::impl_object(&ast);
gen.parse().unwrap()
}

View file

@ -0,0 +1,44 @@
use juniper::{self, execute, GraphQLType, Value, Variables, EmptyMutation, RootNode};
#[derive(GraphQLObject, Debug, PartialEq)]
#[graphql(name="MyObj", description="obj descr")]
struct Obj {
regular_field: bool,
#[graphql(name="renamedField", description="descr", deprecation="field descr")]
c: i32,
}
struct Query;
graphql_object!(Query: () |&self| {
field obj() -> Obj {
Obj{
regular_field: true,
c: 22,
}
}
});
#[test]
fn test_derived_object() {
assert_eq!(Obj::name(), Some("MyObj"));
let doc = r#"
{
obj {
regularField
renamedField
}
}"#;
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
assert_eq!(
execute(doc, None, &schema, &Variables::new(), &()),
Ok((Value::object(vec![
("obj", Value::object(vec![
("regularField", Value::boolean(true)),
("renamedField", Value::int(22)),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}

View file

@ -1,2 +1,3 @@
mod enums; mod enums;
mod input_objects; mod input_objects;
mod derive_object;