From ac01b8e406cb02b138507399611ede3e0dec6dfe Mon Sep 17 00:00:00 2001 From: Magnus Hallin Date: Sun, 9 Oct 2016 14:30:29 +0200 Subject: [PATCH] Extend enum macro, improve test coverage This adds description and deprecation support to the graphql_enum! macro, as well as extensive tests for all syntactical cases. --- src/macros/enums.rs | 117 +++++++++++-- src/macros/mod.rs | 2 + src/macros/tests/enums.rs | 349 ++++++++++++++++++++++++++++++++++++++ src/macros/tests/mod.rs | 1 + 4 files changed, 454 insertions(+), 15 deletions(-) create mode 100644 src/macros/tests/enums.rs create mode 100644 src/macros/tests/mod.rs diff --git a/src/macros/enums.rs b/src/macros/enums.rs index 3c3a9de7..25585e80 100644 --- a/src/macros/enums.rs +++ b/src/macros/enums.rs @@ -35,19 +35,45 @@ you can write `graphql_enum!(Color as "MyColor" { ...`. macro_rules! graphql_enum { ( @as_expr, $e:expr) => { $e }; ( @as_pattern, $p:pat) => { $p }; + ( @as_path, $p:path) => { $p }; - // EnumName as "__ExportedNmae" { Enum::Value => "STRING_VALUE", } - // with no trailing comma - ( $name:path as $outname:tt { $($eval:path => $ename:tt),* }) => { + // 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) }; + + // Each of the @parse match arms accumulates data up to a call to @generate. + // + // ( $name, $outname, $descr ): the name of the Rust enum, the name of the + // GraphQL enum (as a string), and the description of the enum (as a string or None) + // + // [ ( $eval, $ename, $edescr, $edepr ) , ] the value of the Rust enum, + // the value of the GraphQL enum (as a string), the description of the enum + // value (as a string or None), and the deprecation reason of the enum value + // (as a string or None). + ( + @generate, + ( $name:path, $outname:tt, $descr:tt ), + [ $( ( $eval:tt, $ename:tt, $edescr:tt, $edepr:tt ) , )* ] + ) => { impl $crate::GraphQLType for $name { fn name() -> Option<&'static str> { Some(graphql_enum!(@as_expr, $outname)) } fn meta(registry: &mut $crate::Registry) -> $crate::meta::MetaType { - registry.build_enum_type::<$name>()(&[ - $( $crate::meta::EnumValue::new(graphql_enum!(@as_expr, $ename)) ),* - ]) + graphql_enum!( + @maybe_apply, $descr, description, + registry.build_enum_type::<$name>()(&[ + $( + graphql_enum!( + @maybe_apply, + $edepr, deprecated, + graphql_enum!( + @maybe_apply, + $edescr, description, + $crate::meta::EnumValue::new(graphql_enum!(@as_expr, $ename)))) + ),* + ])) .into_meta() } @@ -82,18 +108,79 @@ macro_rules! graphql_enum { } }; - // Same as above, *with* trailing comma - ( $name:path as $outname:tt { $($eval:path => $ename:tt, )* }) => { - graphql_enum!($name as $outname { $( $eval => $ename ),* }); + // No more items to parse + ( @parse, $meta:tt, $acc:tt, ) => { + graphql_enum!( @generate, $meta, $acc ); }; - // Default named enum, without trailing comma - ( $name:path { $($eval:path => $ename:tt),* }) => { - graphql_enum!($name as (stringify!($name)) { $( $eval => $ename ),* }); + // Remove extraneous commas + ( @parse, $meta:tt, $acc:tt, , $($rest:tt)* ) => { + graphql_enum!( @parse, $meta, $acc, $($rest)* ); }; - // Default named enum, with trailing comma - ( $name:path { $($eval:path => $ename:tt, )* }) => { - graphql_enum!($name as (stringify!($name)) { $( $eval => $ename ),* }); + // description: + ( + @parse, + ( $name:tt, $outname:tt, $_ignore:tt ), + $acc:tt, + description: $descr:tt $($items:tt)* + ) => { + graphql_enum!( @parse, ( $name, $outname, $descr ), $acc, $($items)* ); + }; + + // RustEnumValue => "GraphQL enum value" deprecated + ( + @parse, + $meta:tt, + [ $($acc:tt ,)* ], + $eval:path => $ename:tt deprecated $depr:tt $($rest:tt)* + ) => { + graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, None, $depr ), ], $($rest)* ); + }; + + // RustEnumValue => "GraphQL enum value" as deprecated + ( + @parse, + $meta:tt, + [ $($acc:tt ,)* ], + $eval:path => $ename:tt as $descr:tt deprecated $depr:tt $($rest:tt)* + ) => { + graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, $descr, $depr ), ], $($rest)* ); + }; + + // RustEnumValue => "GraphQL enum value" as + ( + @parse, + $meta:tt, + [ $($acc:tt ,)* ], + $eval:path => $ename:tt as $descr:tt $($rest:tt)* + ) => { + graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, $descr, None ), ], $($rest)* ); + }; + + // RustEnumValue => "GraphQL enum value" + ( + @parse, + $meta:tt, + [ $($acc:tt ,)* ], + $eval:path => $ename:tt $($rest:tt)* + ) => { + graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval , $ename , None , None ), ], $($rest)* ); + }; + + // Entry point: + // RustEnumName as "GraphQLEnumName" { ... } + ( + $name:path as $outname:tt { $($items:tt)* } + ) => { + graphql_enum!( @parse, ( $name, $outname, None ), [ ], $($items)* ); + }; + + // Entry point + // RustEnumName { ... } + ( + $name:path { $($items:tt)* } + ) => { + graphql_enum!( @parse, ( $name, (stringify!($name)), None ), [ ], $($items)* ); }; } diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 1a7db6ab..b82f74a7 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -4,3 +4,5 @@ #[macro_use] mod scalar; #[macro_use] mod args; #[macro_use] mod field; + +#[cfg(test)] mod tests; diff --git a/src/macros/tests/enums.rs b/src/macros/tests/enums.rs new file mode 100644 index 00000000..5b667dc5 --- /dev/null +++ b/src/macros/tests/enums.rs @@ -0,0 +1,349 @@ +use std::collections::HashMap; + +use executor::FieldResult; +use value::Value; +use schema::model::RootNode; + + +enum DefaultName { Foo, Bar } +enum Named { Foo, Bar } +enum NoTrailingComma { Foo, Bar } +enum EnumDescription { Foo, Bar } +enum EnumValueDescription { Foo, Bar } +enum EnumDeprecation { Foo, Bar } + +struct Root; + +/* + +Syntax to validate: + +* Default name vs. custom name +* Trailing comma vs. no trailing comma +* Description vs. no description on the enum +* Description vs. no description on the enum values themselves +* Deprecation on enum fields + +*/ + +graphql_enum!(DefaultName { + DefaultName::Foo => "FOO", + DefaultName::Bar => "BAR", +}); + +graphql_enum!(Named as "ANamedEnum" { + Named::Foo => "FOO", + Named::Bar => "BAR", +}); + +graphql_enum!(NoTrailingComma { + NoTrailingComma::Foo => "FOO", + NoTrailingComma::Bar => "BAR" +}); + +graphql_enum!(EnumDescription { + description: "A description of the enum itself" + + EnumDescription::Foo => "FOO", + EnumDescription::Bar => "BAR", +}); + +graphql_enum!(EnumValueDescription { + EnumValueDescription::Foo => "FOO" as "The FOO value", + EnumValueDescription::Bar => "BAR" as "The BAR value", +}); + +graphql_enum!(EnumDeprecation { + EnumDeprecation::Foo => "FOO" deprecated "Please don't use FOO any more", + EnumDeprecation::Bar => "BAR" as "The BAR value" deprecated "Please don't use BAR any more", +}); + +graphql_object!(Root: () as "Root" |&self| { + field default_name() -> FieldResult { Ok(DefaultName::Foo) } + field named() -> FieldResult { Ok(Named::Foo) } + field no_trailing_comma() -> FieldResult { Ok(NoTrailingComma::Foo) } + field enum_description() -> FieldResult { Ok(EnumDescription::Foo) } + field enum_value_description() -> FieldResult { Ok(EnumValueDescription::Foo) } + field enum_deprecation() -> FieldResult { Ok(EnumDeprecation::Foo) } +}); + +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 values = type_info + .get("enumValues").expect("enumValues field missing") + .as_list_value().expect("enumValues not a list"); + + f((type_info, values)); +} + +#[test] +fn default_name_introspection() { + let doc = r#" + { + __type(name: "DefaultName") { + name + description + enumValues { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("DefaultName"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(values.len(), 2); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("FOO")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("BAR")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn named_introspection() { + let doc = r#" + { + __type(name: "ANamedEnum") { + name + description + enumValues { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("ANamedEnum"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(values.len(), 2); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("FOO")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("BAR")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn no_trailing_comma_introspection() { + let doc = r#" + { + __type(name: "NoTrailingComma") { + name + description + enumValues { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("NoTrailingComma"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(values.len(), 2); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("FOO")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("BAR")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn enum_description_introspection() { + let doc = r#" + { + __type(name: "EnumDescription") { + name + description + enumValues { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("EnumDescription"))); + assert_eq!(type_info.get("description"), Some(&Value::string("A description of the enum itself"))); + + assert_eq!(values.len(), 2); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("FOO")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("BAR")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn enum_value_description_introspection() { + let doc = r#" + { + __type(name: "EnumValueDescription") { + name + description + enumValues { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("EnumValueDescription"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(values.len(), 2); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("FOO")), + ("description", Value::string("The FOO value")), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("BAR")), + ("description", Value::string("The BAR value")), + ("isDeprecated", Value::boolean(false)), + ("deprecationReason", Value::null()), + ].into_iter().collect()))); + }); +} + +#[test] +fn enum_deprecation_introspection() { + let doc = r#" + { + __type(name: "EnumDeprecation") { + name + description + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("EnumDeprecation"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(values.len(), 2); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("FOO")), + ("description", Value::null()), + ("isDeprecated", Value::boolean(true)), + ("deprecationReason", Value::string("Please don't use FOO any more")), + ].into_iter().collect()))); + + assert!(values.contains(&Value::object(vec![ + ("name", Value::string("BAR")), + ("description", Value::string("The BAR value")), + ("isDeprecated", Value::boolean(true)), + ("deprecationReason", Value::string("Please don't use BAR any more")), + ].into_iter().collect()))); + }); +} + +#[test] +fn enum_deprecation_no_values_introspection() { + let doc = r#" + { + __type(name: "EnumDeprecation") { + name + description + enumValues { + name + description + isDeprecated + deprecationReason + } + } + } + "#; + + run_type_info_query(doc, |(type_info, values)| { + assert_eq!(type_info.get("name"), Some(&Value::string("EnumDeprecation"))); + assert_eq!(type_info.get("description"), Some(&Value::null())); + + assert_eq!(values.len(), 0); + }); +} diff --git a/src/macros/tests/mod.rs b/src/macros/tests/mod.rs new file mode 100644 index 00000000..3c89105e --- /dev/null +++ b/src/macros/tests/mod.rs @@ -0,0 +1 @@ +mod enums;