diff --git a/README.md b/README.md index ad45ff9d..06e3c1e4 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ as well. * [ ] Sending input objects and partial input objects in variables * [ ] Sending enums in variables * [ ] General input value type checking and validation -* [ ] Improve helper macros - * [ ] `graphql_union!` helper completely missing +* [X] Improve helper macros + * [X] `graphql_union!` helper completely missing * [X] `graphql_input_object!` helper completely missing * [X] Add support for deprecating things * [X] Custom enum values and descriptions diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 7a2438c4..cac41ddb 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -5,5 +5,6 @@ #[macro_use] mod args; #[macro_use] mod field; #[macro_use] mod input_object; +#[macro_use] mod union; #[cfg(test)] mod tests; diff --git a/src/macros/tests/mod.rs b/src/macros/tests/mod.rs index 769beeec..a0831de9 100644 --- a/src/macros/tests/mod.rs +++ b/src/macros/tests/mod.rs @@ -5,3 +5,4 @@ mod args; mod field; mod object; mod interface; +mod union; diff --git a/src/macros/tests/union.rs b/src/macros/tests/union.rs new file mode 100644 index 00000000..d772acb1 --- /dev/null +++ b/src/macros/tests/union.rs @@ -0,0 +1,219 @@ +use std::collections::HashMap; +use std::marker::PhantomData; + +use ast::InputValue; +use value::Value; +use schema::model::RootNode; + +/* + +Syntax to validate: + +* Order of items: description, instance resolvers +* Optional Generics/lifetimes +* Custom name vs. default name +* Optional commas between items +* Optional trailing commas on instance resolvers + + */ + +struct Concrete; + +enum DefaultName { Concrete(Concrete) } + +enum WithLifetime<'a> { Int(PhantomData<&'a i64>) } +enum WithGenerics { Generic(T) } + +enum DescriptionFirst { Concrete(Concrete) } +enum ResolversFirst { Concrete(Concrete) } + +enum CommasWithTrailing { Concrete(Concrete) } +enum ResolversWithTrailingComma { Concrete(Concrete) } + +struct Root; + +graphql_object!(Concrete: () |&self| { + field simple() -> i64 { 123 } +}); + +graphql_union!(DefaultName: () |&self| { + instance_resolvers: |&_| { + Concrete => match *self { DefaultName::Concrete(ref c) => Some(c) } + } +}); + +graphql_union!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| { + instance_resolvers: |&_| { + Concrete => match *self { WithLifetime::Int(_) => Some(Concrete) } + } +}); + +graphql_union!( WithGenerics: () as "WithGenerics" |&self| { + instance_resolvers: |&_| { + Concrete => match *self { WithGenerics::Generic(_) => Some(Concrete) } + } +}); + +graphql_union!(DescriptionFirst: () |&self| { + description: "A description" + instance_resolvers: |&_| { + Concrete => match *self { DescriptionFirst::Concrete(ref c) => Some(c) } + } +}); + +graphql_union!(ResolversFirst: () |&self| { + instance_resolvers: |&_| { + Concrete => match *self { ResolversFirst::Concrete(ref c) => Some(c) } + } + description: "A description" +}); + +graphql_union!(CommasWithTrailing: () |&self| { + instance_resolvers: |&_| { + Concrete => match *self { CommasWithTrailing::Concrete(ref c) => Some(c) } + }, + description: "A description", +}); + +graphql_union!(ResolversWithTrailingComma: () |&self| { + instance_resolvers: |&_| { + Concrete => match *self { ResolversWithTrailingComma::Concrete(ref c) => Some(c) }, + } + description: "A description" +}); + +graphql_object!(<'a> Root: () as "Root" |&self| { + field default_name() -> DefaultName { DefaultName::Concrete(Concrete) } + field with_lifetime() -> WithLifetime<'a> { WithLifetime::Int(PhantomData) } + field with_generics() -> WithGenerics { WithGenerics::Generic(123) } + field description_first() -> DescriptionFirst { DescriptionFirst::Concrete(Concrete) } + field resolvers_first() -> ResolversFirst { ResolversFirst::Concrete(Concrete) } + field commas_with_trailing() -> CommasWithTrailing { CommasWithTrailing::Concrete(Concrete) } + field resolvers_with_trailing_comma() -> ResolversWithTrailingComma { + ResolversWithTrailingComma::Concrete(Concrete) + } +}); + + +fn run_type_info_query(type_name: &str, f: F) + where F: Fn(&HashMap, &Vec) -> () +{ + let doc = r#" + query ($typeName: String!) { + __type(name: $typeName) { + name + description + possibleTypes { + name + } + } + } + "#; + let schema = RootNode::new(Root {}, ()); + let vars = vec![ + ("typeName".to_owned(), InputValue::string(type_name)), + ].into_iter().collect(); + + let (result, errs) = ::execute(doc, None, &schema, &vars, &()) + .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 possible_types = type_info + .get("possibleTypes").expect("possibleTypes field missing") + .as_list_value().expect("possibleTypes field not a list value"); + + f(type_info, possible_types); +} + + +#[test] +fn introspect_default_name() { + run_type_info_query("DefaultName", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("DefaultName"))); + assert_eq!(union.get("description"), Some(&Value::null())); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_with_lifetime() { + run_type_info_query("WithLifetime", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("WithLifetime"))); + assert_eq!(union.get("description"), Some(&Value::null())); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_with_generics() { + run_type_info_query("WithGenerics", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("WithGenerics"))); + assert_eq!(union.get("description"), Some(&Value::null())); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_description_first() { + run_type_info_query("DescriptionFirst", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("DescriptionFirst"))); + assert_eq!(union.get("description"), Some(&Value::string("A description"))); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_resolvers_first() { + run_type_info_query("ResolversFirst", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("ResolversFirst"))); + assert_eq!(union.get("description"), Some(&Value::string("A description"))); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_commas_with_trailing() { + run_type_info_query("CommasWithTrailing", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("CommasWithTrailing"))); + assert_eq!(union.get("description"), Some(&Value::string("A description"))); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_resolvers_with_trailing_comma() { + run_type_info_query("ResolversWithTrailingComma", |union, possible_types| { + assert_eq!(union.get("name"), Some(&Value::string("ResolversWithTrailingComma"))); + assert_eq!(union.get("description"), Some(&Value::string("A description"))); + + assert!(possible_types.contains(&Value::object(vec![ + ("name", Value::string("Concrete")), + ].into_iter().collect()))); + }); +} diff --git a/src/macros/union.rs b/src/macros/union.rs new file mode 100644 index 00000000..362f0e25 --- /dev/null +++ b/src/macros/union.rs @@ -0,0 +1,160 @@ +#[macro_export] +macro_rules! graphql_union { + ( @as_item, $i:item) => { $i }; + ( @as_expr, $e:expr) => { $e }; + ( @as_path, $p:path) => { $p }; + ( @as_type, $t:ty) => { $t }; + + // description: + ( + @ gather_meta, + ($reg:expr, $acc:expr, $descr:expr), + description : $value:tt $( $rest:tt )* + ) => { + $descr = Some(graphql_interface!(@as_expr, $value)); + + graphql_union!(@ gather_meta, ($reg, $acc, $descr), $( $rest )*) + }; + + // Gathering meta for instance resolvers + // instance_resolvers: | | [...] + ( + @ gather_meta, + ($reg:expr, $acc:expr, $descr:expr), + instance_resolvers: | $ctxtvar:pat | { $( $srctype:ty => $resolver:expr ),* $(,)* } $( $rest:tt )* + ) => { + $acc = vec![ + $( + $reg.get_type::<$srctype>() + ),* + ]; + + graphql_union!(@ gather_meta, ($reg, $acc, $descr), $( $rest )*) + }; + + // To generate the "concrete type name" resolver, syntax case: + // instance_resolvers: | | [...] + ( + @ concrete_type_name, + ($outname:tt, $ctxtarg:ident, $ctxttype:ty), + instance_resolvers: | $ctxtvar:pat | { $( $srctype:path => $resolver:expr ),* $(,)* } $( $rest:tt )* + ) => { + let $ctxtvar = &$ctxtarg; + + $( + if let Some(_) = $resolver { + return (<$srctype as $crate::GraphQLType<$ctxttype>>::name()).unwrap().to_owned(); + } + )* + + panic!("Concrete type not handled by instance resolvers on {}", $outname); + }; + + // To generate the "resolve into type" resolver, syntax case: + // instance_resolvers: | | [...] + ( + @ resolve_into_type, + ($outname:tt, $typenamearg:ident, $execarg:ident, $ctxttype:ty), + instance_resolvers: | $ctxtvar:pat | { $( $srctype:path => $resolver:expr ),* $(,)* } $( $rest:tt )* + ) => { + let $ctxtvar = &$execarg.context(); + + $( + if $typenamearg == (<$srctype as $crate::GraphQLType<$ctxttype>>::name()).unwrap().to_owned() { + return $execarg.resolve(&$resolver); + } + )* + + panic!("Concrete type not handled by instance resolvers on {}", $outname); + }; + + // eat commas + ( @ $mfn:ident, $args:tt, , $($rest:tt)* ) => { + graphql_union!(@ $mfn, $args, $($rest)*); + }; + + // eat one tt + ( @ $mfn:ident, $args:tt, $item:tt $($rest:tt)* ) => { + graphql_union!(@ $mfn, $args, $($rest)*); + }; + + // end case + ( @ $mfn:ident, $args:tt, ) => {}; + + ( + ( $($lifetime:tt),* ) $name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | { + $( $items:tt )* + } + ) => { + graphql_union!(@as_item, impl<$($lifetime)*> $crate::GraphQLType<$ctxt> for $name { + fn name() -> Option<&'static str> { + Some($outname) + } + + #[allow(unused_assignments)] + #[allow(unused_mut)] + fn meta(registry: &mut $crate::Registry<$ctxt>) -> $crate::meta::MetaType { + let mut types; + let mut description = None; + graphql_union!(@ gather_meta, (registry, types, description), $($items)*); + let mut mt = registry.build_union_type::<$name>()(&types); + + if let Some(description) = description { + mt = mt.description(description); + } + + mt.into_meta() + } + + fn concrete_type_name(&$mainself, context: &$ctxt) -> String { + graphql_union!( + @ concrete_type_name, + ($outname, context, $ctxt), + $($items)*); + } + + fn resolve_into_type( + &$mainself, + type_name: &str, + _: Option>, + executor: &mut $crate::Executor<$ctxt>, + ) -> $crate::ExecutionResult { + graphql_union!( + @ resolve_into_type, + ($outname, type_name, executor, $ctxt), + $($items)*); + } + }); + + impl<$($lifetime)*> $crate::IntoFieldResult<$name> for $name { + fn into(self) -> $crate::FieldResult<$name> { + Ok(self) + } + } + }; + + ( + <$($lifetime:tt),*> $name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | { + $( $items:tt )* + } + ) => { + graphql_union!( + ($($lifetime),*) $name : $ctxt as $outname | &$mainself | { $( $items )* }); + }; + + ( + $name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | { + $( $items:tt )* + } + ) => { + graphql_union!(() $name : $ctxt as $outname | &$mainself | { $( $items )* }); + }; + + ( + $name:ty : $ctxt:ty | &$mainself:ident | { + $( $items:tt )* + } + ) => { + graphql_union!(() $name : $ctxt as (stringify!($name)) | &$mainself | { $( $items )* }); + }; +}