(codegen) improve derive for InputObject

* Implement hack to allow usage in juniper crate
* implement (default) attribute for Default::default()
* Improve tests
This commit is contained in:
theduke 2017-12-02 15:02:17 +01:00
parent 45859bf405
commit 643875838d
3 changed files with 115 additions and 27 deletions

View file

@ -9,6 +9,7 @@ use util::*;
struct ObjAttrs { struct ObjAttrs {
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
internal: bool,
} }
impl ObjAttrs { impl ObjAttrs {
@ -26,6 +27,15 @@ impl ObjAttrs {
res.description = Some(val); res.description = Some(val);
continue; continue;
} }
match item {
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
if ident == "_internal" {
res.internal = true;
continue;
}
},
_ => {},
}
panic!(format!( panic!(format!(
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}", "Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
item item
@ -40,7 +50,8 @@ impl ObjAttrs {
struct ObjFieldAttrs { struct ObjFieldAttrs {
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
default: Option<String>, default: bool,
default_expr: Option<String>,
} }
impl ObjFieldAttrs { impl ObjFieldAttrs {
@ -59,9 +70,18 @@ impl ObjFieldAttrs {
continue; continue;
} }
if let Some(val) = keyed_item_value(item, "default", true) { if let Some(val) = keyed_item_value(item, "default", true) {
res.default = Some(val); res.default_expr = Some(val);
continue; continue;
} }
match item {
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
if ident == "default" {
res.default = true;
continue;
}
}
_ => {},
}
panic!(format!( panic!(format!(
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}", "Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
item item
@ -122,14 +142,20 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
None => quote!{ let field = field; }, None => quote!{ let field = field; },
}; };
let default = match field_attrs.default { let default = {
Some(ref def) => match syn::parse_token_trees(def) { if field_attrs.default {
Ok(t) => Some(quote!{ #(#t)* }), Some(quote! { Default::default() } )
Err(_) => { } else {
panic!("#graphql(default = ?) must be a valid Rust expression inside a string"); match field_attrs.default_expr {
Some(ref def) => match syn::parse_token_trees(def) {
Ok(t) => Some(quote! { #(#t)* }),
Err(_) => {
panic!("#graphql(default = ?) must be a valid Rust expression inside a string");
}
},
None => None,
} }
}, }
None => None,
}; };
let create_meta_field = match default { let create_meta_field = match default {
@ -158,7 +184,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
let from_input_default = match default { let from_input_default = match default {
Some(ref def) => { Some(ref def) => {
quote!{ quote!{
Some(&&::juniper::InputValue::Null) | None if true => #def, Some(&&_juniper::InputValue::Null) | None if true => #def,
} }
} }
None => quote!{}, None => quote!{},
@ -169,8 +195,11 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
// TODO: investigate the unwraps here, they seem dangerous! // TODO: investigate the unwraps here, they seem dangerous!
match obj.get(#name) { match obj.get(#name) {
#from_input_default #from_input_default
Some(v) => ::juniper::FromInputValue::from_input_value(v).unwrap(), Some(v) => _juniper::FromInputValue::from_input_value(v).unwrap(),
_ => ::juniper::FromInputValue::from_input_value(&::juniper::InputValue::null()).unwrap() _ => {
_juniper::FromInputValue::from_input_value(&_juniper::InputValue::null())
.unwrap()
},
} }
}, },
}; };
@ -183,8 +212,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
to_inputs.push(to_input); to_inputs.push(to_input);
} }
quote! { let body = quote! {
impl ::juniper::GraphQLType for #ident { impl _juniper::GraphQLType for #ident {
type Context = (); type Context = ();
type TypeInfo = (); type TypeInfo = ();
@ -192,7 +221,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
Some(#name) Some(#name)
} }
fn meta<'r>(_: &(), registry: &mut ::juniper::Registry<'r>) -> ::juniper::meta::MetaType<'r> { fn meta<'r>(_: &(), registry: &mut _juniper::Registry<'r>) -> _juniper::meta::MetaType<'r> {
let fields = &[ let fields = &[
#(#meta_fields)* #(#meta_fields)*
]; ];
@ -202,8 +231,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
} }
} }
impl ::juniper::FromInputValue for #ident { impl _juniper::FromInputValue for #ident {
fn from_input_value(value: &::juniper::InputValue) -> Option<#ident> { fn from_input_value(value: &_juniper::InputValue) -> Option<#ident> {
if let Some(obj) = value.to_object_value() { if let Some(obj) = value.to_object_value() {
let item = #ident { let item = #ident {
#(#from_inputs)* #(#from_inputs)*
@ -216,12 +245,46 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
} }
} }
impl ::juniper::ToInputValue for #ident { impl _juniper::ToInputValue for #ident {
fn to_input_value(&self) -> ::juniper::InputValue { fn to_input_value(&self) -> _juniper::InputValue {
::juniper::InputValue::object(vec![ _juniper::InputValue::object(vec![
#(#to_inputs)* #(#to_inputs)*
].into_iter().collect()) ].into_iter().collect())
} }
} }
} };
let dummy_const = Ident::new(format!("_IMPL_GRAPHQLINPUTOBJECT_FOR_{}", ident));
// This ugly hack makes it possible to use the derive inside juniper itself.
// FIXME: Figure out a better way to do this!
let crate_reference = if attrs.internal {
quote! {
#[doc(hidden)]
mod _juniper {
pub use ::{
InputValue,
FromInputValue,
GraphQLType,
Registry,
meta,
ToInputValue
};
}
}
} else {
quote! {
extern crate juniper as _juniper;
}
};
let generated = quote! {
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
#[doc(hidden)]
const #dummy_const : () = {
#crate_reference
#body
};
};
generated
} }

View file

@ -2,7 +2,7 @@
use fnv::FnvHashMap; use fnv::FnvHashMap;
#[cfg(test)] #[cfg(test)]
use juniper::{self, FromInputValue, GraphQLType, ToInputValue}; use juniper::{self, FromInputValue, GraphQLType, InputValue};
#[derive(GraphQLInputObject, Debug, PartialEq)] #[derive(GraphQLInputObject, Debug, PartialEq)]
#[graphql(name = "MyInput", description = "input descr")] #[graphql(name = "MyInput", description = "input descr")]
@ -10,6 +10,9 @@ struct Input {
regular_field: String, regular_field: String,
#[graphql(name = "haha", default = "33", description = "haha descr")] #[graphql(name = "haha", default = "33", description = "haha descr")]
c: i32, c: i32,
#[graphql(default)]
other: Option<bool>,
} }
#[test] #[test]
@ -22,10 +25,31 @@ fn test_derived_input_object() {
assert_eq!(meta.name(), Some("MyInput")); assert_eq!(meta.name(), Some("MyInput"));
assert_eq!(meta.description(), Some(&"input descr".to_string())); assert_eq!(meta.description(), Some(&"input descr".to_string()));
let obj = Input { // Test default value injection.
regular_field: "a".to_string(),
let input_no_defaults: InputValue = ::serde_json::from_value(json!({
"regularField": "a",
})).unwrap();
let output_no_defaults: Input = FromInputValue::from_input_value(&input_no_defaults).unwrap();
assert_eq!(output_no_defaults, Input{
regular_field: "a".into(),
c: 33, c: 33,
}; other: None,
let restored: Input = FromInputValue::from_input_value(&obj.to_input_value()).unwrap(); });
assert_eq!(obj, restored);
// Test with all values supplied.
let input: InputValue = ::serde_json::from_value(json!({
"regularField": "a",
"haha": 55,
"other": true,
})).unwrap();
let output: Input = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(output, Input{
regular_field: "a".into(),
c: 55,
other: Some(true),
});
} }

View file

@ -1,5 +1,6 @@
#[macro_use] #[macro_use]
extern crate juniper; extern crate juniper;
#[macro_use]
extern crate serde_json; extern crate serde_json;
#[cfg(test)] #[cfg(test)]