diff --git a/src/macros/input_object.rs b/src/macros/input_object.rs new file mode 100644 index 00000000..b78aeb56 --- /dev/null +++ b/src/macros/input_object.rs @@ -0,0 +1,178 @@ +/** +Create an input object + +Input objects are used as data carriers for complex input values to +fields and mutations. Unlike the other helper macros, +`graphql_input_object!` actually *creates* the struct you define. It +does not add anything to the struct definition itself - what you type +is what will be generated: + +```rust +# #[macro_use] extern crate juniper; + +graphql_input_object!( + description: "Coordinates for the user" + + struct Coordinates { + longitude: f64 as "The X coordinate, from -180 to +180", + latitude: f64 as "The Y coordinate, from -90 to +90", + } +); + +# fn main() { } +``` + +This macro creates the struct as specified and implements +`FromInputValue` to automatically parse values provided from variables +and arguments. + +If you want to expose the struct under a different name than the Rust +type, you can write `struct Coordinates as "MyCoordinates" { ...`. + +*/ +#[macro_export] +macro_rules! graphql_input_object { + // Calls $val.$func($arg) if $arg is not None + ( @maybe_apply, None, $func:ident, $val:expr ) => { $val }; + ( @maybe_apply, $arg:tt, $func:ident, $val:expr ) => { $val.$func($arg) }; + + // Calls $val.description($descr) when $descr is not empty + ( @apply_description, , $val:expr ) => { $val }; + ( @apply_description, $descr:tt , $val:expr ) => { $val.description($descr) }; + + // Generate the FromInputValue::from method body, provided a + // HashMap<&str, &InputValue> in $var + ( + @generate_from_input_value, + $name:tt, $var:tt, + ( $($field_name:ident : $field_type:ty $(as $descr:tt)* $(,)* ),* ) + ) => { + Some($name { + $( $field_name: { + let n: String = $crate::to_snake_case(stringify!($field_name)); + let v: &$crate::InputValue = $var[&n[..]]; + $crate::FromInputValue::from(v).unwrap() + } ),* + }) + }; + + // Generate the struct declaration, including (Rust) meta attributes + ( + @generate_struct_fields, + ( $($meta:tt)* ), $name:tt, + ( $($field_name:ident : $field_type:ty $(as $descr:tt)* $(,)* ),* ) + ) => { + $($meta)* struct $name { + $( $field_name: $field_type, )* + } + }; + + // Generate the input field meta list, i.e. &[Argument]. + ( + @generate_meta_fields, + $reg:tt, + ( $($field_name:ident : $field_type:ty $(as $descr:tt)* $(,)* ),* ) + ) => { + &[ + $( + graphql_input_object!( + @apply_description, + $($descr)*, + $reg.arg::<$field_type>( + &$crate::to_snake_case(stringify!($field_name)))) + ),* + ] + }; + + // #[...] struct $name { ... } + // struct $name { ... } + ( + @parse, + ( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $descr:tt ), + $(#[$meta:meta])* struct $name:ident { $($fields:tt)* } $($rest:tt)* + ) => { + graphql_input_object!( + @parse, + ( ( $(#[$meta])* ), $name, (stringify!($name)), ($($fields)*), $descr ), + $($rest)* + ); + }; + + // #[...] struct $name as "GraphQLName" { ... } + // struct $name as "GraphQLName" { ... } + ( + @parse, + ( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $descr:tt ), + $(#[$meta:meta])* struct $name:ident as $outname:tt { $($fields:tt)* } $($rest:tt)* + ) => { + graphql_input_object!( + @parse, + ( ( $($meta)* ), $name, $outname, ($($fields)*), $descr ), + $($rest)* + ); + }; + + // description: + ( + @parse, + ( $meta:tt, $name:tt, $outname:tt, $fields:tt, $_ignore:tt ), + description: $descr:tt $($rest:tt)* + ) => { + graphql_input_object!( + @parse, + ( $meta, $name, $outname, $fields, $descr ), + $($rest)* + ); + }; + + // No more data to parse, generate the struct and impls + ( + @parse, + ( $meta:tt, $name:tt, $outname:tt, $fields:tt, $descr:tt ), + ) => { + graphql_input_object!(@generate_struct_fields, $meta, $name, $fields); + + impl $crate::FromInputValue for $name { + fn from(value: &$crate::InputValue) -> Option<$name> { + if let Some(obj) = value.to_object_value() { + graphql_input_object!(@generate_from_input_value, $name, obj, $fields) + } + else { + None + } + } + } + + impl $crate::GraphQLType for $name { + fn name() -> Option<&'static str> { + Some($outname) + } + + fn meta(registry: &mut $crate::Registry) -> $crate::meta::MetaType { + graphql_input_object!( + @maybe_apply, $descr, description, + registry.build_input_object_type::<$name>()( + graphql_input_object!(@generate_meta_fields, registry, $fields) + )).into_meta() + } + } + }; + + // Entry point: parse calls starting with the struct declaration + ( $(#[$meta:meta])* struct $($items:tt)* ) => { + graphql_input_object!( + @parse, + ( ( ), None, None, None, None ), + $(#[$meta])* struct $($items)* + ); + }; + + // Entry point: parse calls starting with the description + ( description: $($items:tt)* ) => { + graphql_input_object!( + @parse, + ( ( ), None, None, None, None ), + description: $($items)* + ); + }; +} diff --git a/src/macros/mod.rs b/src/macros/mod.rs index b82f74a7..7a2438c4 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -4,5 +4,6 @@ #[macro_use] mod scalar; #[macro_use] mod args; #[macro_use] mod field; +#[macro_use] mod input_object; #[cfg(test)] mod tests; diff --git a/src/macros/tests/input_object.rs b/src/macros/tests/input_object.rs new file mode 100644 index 00000000..86ab5573 --- /dev/null +++ b/src/macros/tests/input_object.rs @@ -0,0 +1,383 @@ +use std::collections::HashMap; + +use ast::{InputValue, FromInputValue}; +use executor::FieldResult; +use value::Value; +use schema::model::RootNode; + +struct Root; + +graphql_input_object!( + struct DefaultName { + field_one: String, + field_two: String, + } +); + +graphql_input_object!( + struct NoTrailingComma { + field_one: String, + field_two: String + } +); + +graphql_input_object!( + #[derive(Debug)] + struct Derive { + field_one: String, + } +); + +graphql_input_object!( + struct Named as "ANamedInputObject" { + field_one: String, + } +); + +graphql_input_object!( + description: "Description for the input object" + + struct Description { + field_one: String, + } +); + +graphql_input_object!( + struct FieldDescription { + field_one: String as "The first field", + field_two: String as "The second field", + } +); + +graphql_object!(Root: () as "Root" |&self| { + field test_field( + a1: DefaultName, + a2: NoTrailingComma, + a3: Derive, + a4: Named, + a5: Description, + a6: FieldDescription + ) -> FieldResult { + Ok(0) + } +}); + +fn run_type_info_query(doc: &str, f: F) where F: Fn(&HashMap, &Vec) -> () { + let schema = RootNode::new(Root {}, ()); + + let (result, errs) = ::execute(doc, None, &schema, &HashMap::new(), &()) + .expect("Execution failed"); + + assert_eq!(errs, []); + + println!("Result: {:?}", result); + + let type_info = result + .as_object_value().expect("Result is not an object") + .get("__type").expect("__type field missing") + .as_object_value().expect("__type field not an object value"); + + let fields = type_info + .get("inputFields").expect("inputFields field missing") + .as_list_value().expect("inputFields not a list"); + + f(type_info, fields); +} + +#[test] +fn default_name_introspection() { + let doc = r#" + { + __type(name: "DefaultName") { + name + description + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + } + "#; + + run_type_info_query(doc, |type_info, fields| { + assert_eq!(type_info.get("name"), Some(&Value::string("DefaultName"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(fields.len(), 2); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldOne")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldTwo")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn default_name_input_value() { + let iv = InputValue::object(vec![ + ("fieldOne", InputValue::string("number one")), + ("fieldTwo", InputValue::string("number two")), + ].into_iter().collect()); + + let dv: Option = FromInputValue::from(&iv); + + assert!(dv.is_some()); + + let dv = dv.unwrap(); + + assert_eq!(dv.field_one, "number one"); + assert_eq!(dv.field_two, "number two"); +} + +#[test] +fn no_trailing_comma_introspection() { + let doc = r#" + { + __type(name: "NoTrailingComma") { + name + description + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + } + "#; + + run_type_info_query(doc, |type_info, fields| { + assert_eq!(type_info.get("name"), Some(&Value::string("NoTrailingComma"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(fields.len(), 2); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldOne")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldTwo")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn derive_introspection() { + let doc = r#" + { + __type(name: "Derive") { + name + description + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + } + "#; + + run_type_info_query(doc, |type_info, fields| { + assert_eq!(type_info.get("name"), Some(&Value::string("Derive"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(fields.len(), 1); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldOne")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn derive_derived() { + assert_eq!( + format!("{:?}", Derive { field_one: "test".to_owned() }), + "Derive { field_one: \"test\" }" + ); +} + +#[test] +fn named_introspection() { + let doc = r#" + { + __type(name: "ANamedInputObject") { + name + description + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + } + "#; + + run_type_info_query(doc, |type_info, fields| { + assert_eq!(type_info.get("name"), Some(&Value::string("ANamedInputObject"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(fields.len(), 1); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldOne")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn description_introspection() { + let doc = r#" + { + __type(name: "Description") { + name + description + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + } + "#; + + run_type_info_query(doc, |type_info, fields| { + assert_eq!(type_info.get("name"), Some(&Value::string("Description"))); + assert_eq!(type_info.get("description"), Some(&Value::string("Description for the input object"))); + + assert_eq!(fields.len(), 1); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldOne")), + ("description", Value::null()), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn field_description_introspection() { + let doc = r#" + { + __type(name: "FieldDescription") { + name + description + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + } + "#; + + run_type_info_query(doc, |type_info, fields| { + assert_eq!(type_info.get("name"), Some(&Value::string("FieldDescription"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(fields.len(), 2); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldOne")), + ("description", Value::string("The first field")), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("fieldTwo")), + ("description", Value::string("The second field")), + ("type", Value::object(vec![ + ("ofType", Value::object(vec![ + ("name", Value::string("String")), + ].into_iter().collect())), + ].into_iter().collect())), + ("defaultValue", Value::null()), + ].into_iter().collect()))); + }); +} diff --git a/src/macros/tests/mod.rs b/src/macros/tests/mod.rs index d64d8101..96882a73 100644 --- a/src/macros/tests/mod.rs +++ b/src/macros/tests/mod.rs @@ -1,2 +1,3 @@ mod enums; mod scalar; +#[allow(dead_code)] mod input_object;