diff --git a/src/macros/field.rs b/src/macros/field.rs index 2f1d04ed..8ba74b65 100644 --- a/src/macros/field.rs +++ b/src/macros/field.rs @@ -59,6 +59,10 @@ macro_rules! __graphql__build_field_matches { __graphql__build_field_matches!($resolveargs, $acc, $( $rest )*); }; + ( $resolveargs:tt, $acc:tt, , $( $rest:tt )*) => { + __graphql__build_field_matches!($resolveargs, $acc, $( $rest )*); + }; + ( ($outname:tt, $selfvar:ident, $fieldvar:ident, $argsvar:ident, $executorvar:ident), ( $( ( $name:ident; ( $($args:tt)* ); $t:ty; $body:block ) )* ), diff --git a/src/macros/object.rs b/src/macros/object.rs index 7cf1e725..77d9e62e 100644 --- a/src/macros/object.rs +++ b/src/macros/object.rs @@ -333,10 +333,18 @@ macro_rules! graphql_object { graphql_object!(@gather_object_meta, $reg, $acc, $descr, $ifaces, $( $rest )*) }; + // eat commas + ( + @gather_object_meta, + $reg:expr, $acc:expr, $descr:expr, $ifaces:expr, , $( $rest:tt )* + ) => { + graphql_object!(@gather_object_meta, $reg, $acc, $descr, $ifaces, $( $rest )*) + }; + // base case ( @gather_object_meta, - $reg:expr, $acc:expr, $descr:expr, $ifaces:expr, $(,)* + $reg:expr, $acc:expr, $descr:expr, $ifaces:expr, ) => {}; ( @assign_interfaces, $reg:expr, $tgt:expr, [ $($t:ty,)* ] ) => { @@ -418,4 +426,13 @@ macro_rules! graphql_object { graphql_object!( ( ); $name; $ctxt; $outname; $mainself; $( $items )*); }; + + ( + $name:ty : $ctxt:ty | &$mainself:ident | { + $( $items:tt )* + } + ) => { + graphql_object!( + ( ); $name; $ctxt; (stringify!($name)); $mainself; $( $items )*); + }; } diff --git a/src/macros/tests/mod.rs b/src/macros/tests/mod.rs index f0b9274e..b361bc7e 100644 --- a/src/macros/tests/mod.rs +++ b/src/macros/tests/mod.rs @@ -3,3 +3,4 @@ mod scalar; #[allow(dead_code)] mod input_object; mod args; mod field; +mod object; diff --git a/src/macros/tests/object.rs b/src/macros/tests/object.rs new file mode 100644 index 00000000..36157273 --- /dev/null +++ b/src/macros/tests/object.rs @@ -0,0 +1,280 @@ +use std::collections::HashMap; + +use ast::InputValue; +use executor::FieldResult; +use value::Value; +use schema::model::RootNode; + +/* + +Syntax to validate: + +* Order of items: fields, description, interfaces +* Optional Generics/lifetimes +* Custom name vs. default name +* Optional commas between items + + */ + +struct Interface; + +struct DefaultName; + +struct WithLifetime; +struct WithGenerics; + +struct DescriptionFirst; +struct FieldsFirst; +struct InterfacesFirst; + +struct CommasWithTrailing; +struct CommasOnMeta; + +struct Root; + +graphql_object!(DefaultName: () |&self| { + field simple() -> FieldResult { Ok(0) } +}); + + +graphql_object!(<'a> &'a WithLifetime: () as "WithLifetime" |&self| { + field simple() -> FieldResult { Ok(0) } +}); + +graphql_object!( WithGenerics: CtxT as "WithGenerics" |&self| { + field simple() -> FieldResult { Ok(0) } +}); + +graphql_interface!(Interface: () as "Interface" |&self| { + field simple() -> FieldResult { Ok(0) } + + instance_resolvers: |_| [ + Some(DescriptionFirst {}), + ] +}); + +graphql_object!(DescriptionFirst: () as "DescriptionFirst" |&self| { + description: "A description" + + field simple() -> FieldResult { Ok(0) } + + interfaces: [Interface] +}); + +graphql_object!(FieldsFirst: () as "FieldsFirst" |&self| { + field simple() -> FieldResult { Ok(0) } + + description: "A description" + + interfaces: [Interface] +}); + +graphql_object!(InterfacesFirst: () as "InterfacesFirst" |&self| { + interfaces: [Interface] + + field simple() -> FieldResult { Ok(0) } + + description: "A description" +}); + +graphql_object!(CommasWithTrailing: () as "CommasWithTrailing" |&self| { + interfaces: [Interface], + + field simple() -> FieldResult { Ok(0) }, + + description: "A description", +}); + + +graphql_object!(CommasOnMeta: () as "CommasOnMeta" |&self| { + interfaces: [Interface], + description: "A description", + + field simple() -> FieldResult { Ok(0) } +}); + +graphql_object!(Root: () as "Root" |&self| { + field default_name() -> FieldResult { Ok(DefaultName {}) } + + field with_lifetime() -> FieldResult<&WithLifetime> { Err("Nope".to_owned()) } + field with_generics() -> FieldResult { Ok(WithGenerics {}) } + + field description_first() -> FieldResult { Ok(DescriptionFirst {}) } + field fields_first() -> FieldResult { Ok(FieldsFirst {}) } + field interfaces_first() -> FieldResult { Ok(InterfacesFirst {}) } + + field commas_with_trailing() -> FieldResult { Ok(CommasWithTrailing {}) } + field commas_on_meta() -> FieldResult { Ok(CommasOnMeta {}) } +}); + + +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 + fields(includeDeprecated: true) { + name + } + interfaces { + name + kind + } + } + } + "#; + 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 fields = type_info + .get("fields").expect("fields field missing") + .as_list_value().expect("fields field not a list value"); + + f(type_info, fields); +} + +#[test] +fn introspect_default_name() { + run_type_info_query("DefaultName", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("DefaultName"))); + assert_eq!(object.get("description"), Some(&Value::null())); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_with_lifetime() { + run_type_info_query("WithLifetime", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("WithLifetime"))); + assert_eq!(object.get("description"), Some(&Value::null())); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_with_generics() { + run_type_info_query("WithGenerics", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("WithGenerics"))); + assert_eq!(object.get("description"), Some(&Value::null())); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_description_first() { + run_type_info_query("DescriptionFirst", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("DescriptionFirst"))); + assert_eq!(object.get("description"), Some(&Value::string("A description"))); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![ + Value::object(vec![ + ("name", Value::string("Interface")), + ("kind", Value::string("INTERFACE")), + ].into_iter().collect()), + ]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_fields_first() { + run_type_info_query("FieldsFirst", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("FieldsFirst"))); + assert_eq!(object.get("description"), Some(&Value::string("A description"))); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![ + Value::object(vec![ + ("name", Value::string("Interface")), + ("kind", Value::string("INTERFACE")), + ].into_iter().collect()), + ]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_interfaces_first() { + run_type_info_query("InterfacesFirst", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("InterfacesFirst"))); + assert_eq!(object.get("description"), Some(&Value::string("A description"))); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![ + Value::object(vec![ + ("name", Value::string("Interface")), + ("kind", Value::string("INTERFACE")), + ].into_iter().collect()), + ]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_commas_with_trailing() { + run_type_info_query("CommasWithTrailing", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("CommasWithTrailing"))); + assert_eq!(object.get("description"), Some(&Value::string("A description"))); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![ + Value::object(vec![ + ("name", Value::string("Interface")), + ("kind", Value::string("INTERFACE")), + ].into_iter().collect()), + ]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +} + +#[test] +fn introspect_commas_on_meta() { + run_type_info_query("CommasOnMeta", |object, fields| { + assert_eq!(object.get("name"), Some(&Value::string("CommasOnMeta"))); + assert_eq!(object.get("description"), Some(&Value::string("A description"))); + assert_eq!(object.get("interfaces"), Some(&Value::list(vec![ + Value::object(vec![ + ("name", Value::string("Interface")), + ("kind", Value::string("INTERFACE")), + ].into_iter().collect()), + ]))); + + assert!(fields.contains(&Value::object(vec![ + ("name", Value::string("simple")), + ].into_iter().collect()))); + }); +}