From 6181b6bcd74121beaf5a9771edceac11e68b7c8c Mon Sep 17 00:00:00 2001 From: Douman Date: Thu, 10 Feb 2022 13:38:28 +0900 Subject: [PATCH] Introduce process_with for input object --- derive/src/args.rs | 2 + derive/src/input_object.rs | 27 +++++++++--- src/docs/input_object.md | 2 + tests/input_object.rs | 85 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/derive/src/args.rs b/derive/src/args.rs index eb0c7d85..0fe983f6 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -372,6 +372,8 @@ pub struct InputObjectField { pub skip: bool, #[darling(default)] pub skip_input: bool, + #[darling(default)] + pub process_with: Option, // for SimpleObject #[darling(default)] pub skip_output: bool, diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 4d62853d..c39bb2c6 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -75,6 +75,16 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult federation_fields.push((ty, name.clone())); + let process_with = match field.process_with.as_ref() { + Some(fn_path) => { + let fn_path: syn::ExprPath = syn::parse_str(&fn_path)?; + quote! { + #fn_path(&mut #ident); + } + } + None => Default::default(), + }; + let validators = field .validator .clone() @@ -99,9 +109,11 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }); get_fields.push(quote! { - let #ident: #ty = #crate_name::InputType::parse( + #[allow(unused_mut)] + let mut #ident: #ty = #crate_name::InputType::parse( ::std::option::Option::Some(#crate_name::Value::Object(::std::clone::Clone::clone(&obj))) ).map_err(#crate_name::InputValueError::propagate)?; + #process_with #validators }); @@ -137,8 +149,12 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult let #ident: #ty = { match obj.get(#name) { ::std::option::Option::Some(value) => { - #crate_name::InputType::parse(::std::option::Option::Some(::std::clone::Clone::clone(&value))) - .map_err(#crate_name::InputValueError::propagate)? + #[allow(unused_mut)] + let mut #ident = #crate_name::InputType::parse(::std::option::Option::Some(::std::clone::Clone::clone(&value))) + .map_err(#crate_name::InputValueError::propagate)?; + #process_with + #ident + }, ::std::option::Option::None => #default, } @@ -147,9 +163,10 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }); } else { get_fields.push(quote! { - #[allow(non_snake_case)] - let #ident: #ty = #crate_name::InputType::parse(obj.get(#name).cloned()) + #[allow(non_snake_case, unused_mut)] + let mut #ident: #ty = #crate_name::InputType::parse(obj.get(#name).cloned()) .map_err(#crate_name::InputValueError::propagate)?; + #process_with #validators }); } diff --git a/src/docs/input_object.md b/src/docs/input_object.md index 5b3cb82f..e02fc03f 100644 --- a/src/docs/input_object.md +++ b/src/docs/input_object.md @@ -24,6 +24,8 @@ Define a GraphQL input object | flatten | Similar to serde (flatten) | boolean | Y | | skip | Skip this field, use `Default::default` to get a default value for this field. | bool | Y | | skip_input | Skip this field, similar to `skip`, but avoids conflicts when this macro is used with `SimpleObject`. | bool | Y | +| process_with | Upon successful parsing, invokes specified function. Its signature must be `fn(&mut T)`. + | code path | Y | | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y | | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y | diff --git a/tests/input_object.rs b/tests/input_object.rs index d9c86ee9..acc3eb44 100644 --- a/tests/input_object.rs +++ b/tests/input_object.rs @@ -546,3 +546,88 @@ pub async fn test_complex_output() { }) ); } + +#[tokio::test] +pub async fn test_input_object_process_with() { + mod processor { + pub fn string(input: &mut String) { + while let Some(ch) = input.pop() { + if !ch.is_whitespace() { + input.push(ch); + break; + } + } + } + } + #[derive(InputObject)] + struct MyInput { + //processor does nothing on default value + #[graphql(default = " ", process_with = "processor::string")] + a: String, + + #[graphql(process_with = "processor::string")] + b: String, + } + + struct MyOutput { + a: String, + b: String, + } + + #[Object] + impl MyOutput { + async fn a(&self) -> &String { + &self.a + } + + async fn b(&self) -> &String { + &self.b + } + } + + struct Root; + + #[Object] + impl Root { + async fn a(&self, input: MyInput) -> MyOutput { + MyOutput { + a: input.a, + b: input.b, + } + } + } + + let schema = Schema::new(Root, EmptyMutation, EmptySubscription); + let query = r#"{ + a(input:{b: "test b "}) { + a b + } + }"# + .to_owned(); + assert_eq!( + schema.execute(&query).await.data, + value!({ + "a": { + "a": " ", + "b": "test b", + } + }) + ); + + let schema = Schema::new(Root, EmptyMutation, EmptySubscription); + let query = r#"{ + a(input:{a: "test a ", b: "test"}) { + a b + } + }"# + .to_owned(); + assert_eq!( + schema.execute(&query).await.data, + value!({ + "a": { + "a": "test a", + "b": "test", + } + }) + ); +}