Add union helper macro and tests
This commit is contained in:
parent
59e5381a56
commit
93deefb0ca
5 changed files with 383 additions and 2 deletions
|
@ -127,8 +127,8 @@ as well.
|
||||||
* [ ] Sending input objects and partial input objects in variables
|
* [ ] Sending input objects and partial input objects in variables
|
||||||
* [ ] Sending enums in variables
|
* [ ] Sending enums in variables
|
||||||
* [ ] General input value type checking and validation
|
* [ ] General input value type checking and validation
|
||||||
* [ ] Improve helper macros
|
* [X] Improve helper macros
|
||||||
* [ ] `graphql_union!` helper completely missing
|
* [X] `graphql_union!` helper completely missing
|
||||||
* [X] `graphql_input_object!` helper completely missing
|
* [X] `graphql_input_object!` helper completely missing
|
||||||
* [X] Add support for deprecating things
|
* [X] Add support for deprecating things
|
||||||
* [X] Custom enum values and descriptions
|
* [X] Custom enum values and descriptions
|
||||||
|
|
|
@ -5,5 +5,6 @@
|
||||||
#[macro_use] mod args;
|
#[macro_use] mod args;
|
||||||
#[macro_use] mod field;
|
#[macro_use] mod field;
|
||||||
#[macro_use] mod input_object;
|
#[macro_use] mod input_object;
|
||||||
|
#[macro_use] mod union;
|
||||||
|
|
||||||
#[cfg(test)] mod tests;
|
#[cfg(test)] mod tests;
|
||||||
|
|
|
@ -5,3 +5,4 @@ mod args;
|
||||||
mod field;
|
mod field;
|
||||||
mod object;
|
mod object;
|
||||||
mod interface;
|
mod interface;
|
||||||
|
mod union;
|
||||||
|
|
219
src/macros/tests/union.rs
Normal file
219
src/macros/tests/union.rs
Normal file
|
@ -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<T> { 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!(<T> WithGenerics<T>: () 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<i64> { 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<F>(type_name: &str, f: F)
|
||||||
|
where F: Fn(&HashMap<String, Value>, &Vec<Value>) -> ()
|
||||||
|
{
|
||||||
|
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())));
|
||||||
|
});
|
||||||
|
}
|
160
src/macros/union.rs
Normal file
160
src/macros/union.rs
Normal file
|
@ -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: <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: | <ctxtvar> | [...]
|
||||||
|
(
|
||||||
|
@ 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: | <ctxtvar> | [...]
|
||||||
|
(
|
||||||
|
@ 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: | <ctxtvar> | [...]
|
||||||
|
(
|
||||||
|
@ 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<Vec<$crate::Selection>>,
|
||||||
|
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 )* });
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue