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:
parent
27048ae7cd
commit
e835a5e019
5 changed files with 236 additions and 0 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
181
juniper_codegen/src/derive_object.rs
Normal file
181
juniper_codegen/src/derive_object.rs
Normal 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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
44
juniper_tests/src/codegen/derive_object.rs
Normal file
44
juniper_tests/src/codegen/derive_object.rs
Normal 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![])));
|
||||||
|
}
|
|
@ -1,2 +1,3 @@
|
||||||
mod enums;
|
mod enums;
|
||||||
mod input_objects;
|
mod input_objects;
|
||||||
|
mod derive_object;
|
||||||
|
|
Loading…
Reference in a new issue