Add input object helper macro
This commit is contained in:
parent
c173554e42
commit
9f5294ad3e
4 changed files with 563 additions and 0 deletions
178
src/macros/input_object.rs
Normal file
178
src/macros/input_object.rs
Normal file
|
@ -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: <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<CtxT> $crate::GraphQLType<CtxT> for $name {
|
||||
fn name() -> Option<&'static str> {
|
||||
Some($outname)
|
||||
}
|
||||
|
||||
fn meta(registry: &mut $crate::Registry<CtxT>) -> $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)*
|
||||
);
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
383
src/macros/tests/input_object.rs
Normal file
383
src/macros/tests/input_object.rs
Normal file
|
@ -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<i64> {
|
||||
Ok(0)
|
||||
}
|
||||
});
|
||||
|
||||
fn run_type_info_query<F>(doc: &str, f: F) where F: Fn(&HashMap<String, Value>, &Vec<Value>) -> () {
|
||||
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<DefaultName> = 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())));
|
||||
});
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
mod enums;
|
||||
mod scalar;
|
||||
#[allow(dead_code)] mod input_object;
|
||||
|
|
Loading…
Reference in a new issue