diff --git a/CHANGELOG.md b/CHANGELOG.md index 740682b9..9ee77dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: * `#[derive(GraphQLInputObject)]` * `#[derive(GraphQLEnum)]` + * `#[derive(GraphQLObject)]` ## [0.8.1] – 2017-06-15 diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs new file mode 100644 index 00000000..a145e097 --- /dev/null +++ b/juniper_codegen/src/derive_object.rs @@ -0,0 +1,181 @@ +use syn; +use syn::*; +use quote::Tokens; + +use ::util::*; + +#[derive(Default, Debug)] +struct ObjAttrs { + name: Option, + description: Option, +} + +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, + description: Option, + deprecation: Option, +} + +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::::new(); + let mut resolvers = Vec::::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) + -> ::juniper::ExecutionResult + { + + match field_name { + #(#resolvers)* + _ => panic!("Field {} not found on type {}", field_name, #ident_name), + } + + } + } + }; + + toks +} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 16375fb9..cc94ce46 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -8,6 +8,7 @@ extern crate quote; mod util; mod enums; mod input_objects; +mod derive_object; use proc_macro::TokenStream; @@ -26,3 +27,11 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { let gen = input_objects::impl_input_object(&ast); 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() +} diff --git a/juniper_tests/src/codegen/derive_object.rs b/juniper_tests/src/codegen/derive_object.rs new file mode 100644 index 00000000..9f27c275 --- /dev/null +++ b/juniper_tests/src/codegen/derive_object.rs @@ -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![]))); +} diff --git a/juniper_tests/src/codegen/mod.rs b/juniper_tests/src/codegen/mod.rs index 33366cf9..5b5e77c8 100644 --- a/juniper_tests/src/codegen/mod.rs +++ b/juniper_tests/src/codegen/mod.rs @@ -1,2 +1,3 @@ mod enums; mod input_objects; +mod derive_object;