From a79be89827c4b96cc16f25e9f2eeb8e44422391f Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Wed, 17 Aug 2022 13:40:22 +0300 Subject: [PATCH 1/7] Add skip_none to codegen and query_derive --- graphql_client_codegen/src/codegen/inputs.rs | 19 ++++-- graphql_client_codegen/src/codegen_options.rs | 13 ++++ graphql_query_derive/src/attributes.rs | 66 +++++++++++++++++++ graphql_query_derive/src/lib.rs | 2 + 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs index eccc0ff6..9c943dd9 100644 --- a/graphql_client_codegen/src/codegen/inputs.rs +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -43,11 +43,20 @@ pub(super) fn generate_input_object_definitions( quote!(#annotation pub #name_ident: #field_type) }); - quote! { - #variable_derives - pub struct #struct_name { - #(#fields,)* - } + match *options.skip_none() { + true => quote! { + #[serde(skip_serializing_if = "Option::is_none")] + #variable_derives + pub struct #struct_name{ + #(#fields,)* + } + }, + false => quote! { + #variable_derives + pub struct #struct_name{ + #(#fields,)* + } + }, } }) .collect() diff --git a/graphql_client_codegen/src/codegen_options.rs b/graphql_client_codegen/src/codegen_options.rs index 1f8f6bcd..4f9bda8a 100644 --- a/graphql_client_codegen/src/codegen_options.rs +++ b/graphql_client_codegen/src/codegen_options.rs @@ -45,6 +45,8 @@ pub struct GraphQLClientCodegenOptions { extern_enums: Vec, /// Flag to trigger generation of Other variant for fragments Enum fragments_other_variant: bool, + /// Skip Serialization of None values. + skip_none: bool, } impl GraphQLClientCodegenOptions { @@ -65,6 +67,7 @@ impl GraphQLClientCodegenOptions { custom_scalars_module: Default::default(), extern_enums: Default::default(), fragments_other_variant: Default::default(), + skip_none: Default::default(), } } @@ -214,4 +217,14 @@ impl GraphQLClientCodegenOptions { pub fn fragments_other_variant(&self) -> &bool { &self.fragments_other_variant } + + /// Set the graphql client codegen option's skip none value. + pub fn set_skip_none(&mut self, skip_none: bool) { + self.skip_none = skip_none + } + + /// Get a reference to the graphql client codegen option's skip none value. + pub fn skip_none(&self) -> &bool { + &self.skip_none + } } diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index fde66a89..5ff10fb6 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -103,6 +103,13 @@ pub fn extract_fragments_other_variant(ast: &syn::DeriveInput) -> bool { .unwrap_or(false) } +pub fn extract_skip_none(ast: &syn::DeriveInput) -> bool { + extract_attr(ast, "skip_none") + .ok() + .and_then(|s| FromStr::from_str(s.as_str()).ok()) + .unwrap_or(false) +} + #[cfg(test)] mod test { use super::*; @@ -219,4 +226,63 @@ mod test { let parsed = syn::parse_str(input).unwrap(); assert!(!extract_fragments_other_variant(&parsed)); } + + #[test] + fn test_skip_none_set_to_true() { + let input = r#" + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + skip_none = "true" + )] + struct MyQuery; + "#; + let parsed = syn::parse_str(input).unwrap(); + assert!(extract_skip_none(&parsed)); + } + + #[test] + fn test_skip_none_set_to_false() { + let input = r#" + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + skip_none = "false" + )] + struct MyQuery; + "#; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_skip_none(&parsed)); + } + + #[test] + fn test_skip_none_set_to_invalid() { + let input = r#" + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + skip_none = "invalid" + )] + struct MyQuery; + "#; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_skip_none(&parsed)); + } + + #[test] + fn test_skip_none_unset() { + let input = r#" + #[derive(GraphQLQuery)] + #[graphql( + schema_path = "x", + query_path = "x", + )] + struct MyQuery; + "#; + let parsed = syn::parse_str(input).unwrap(); + assert!(!extract_skip_none(&parsed)); + } } diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index 360ad0f3..b653aab9 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -64,10 +64,12 @@ fn build_graphql_client_derive_options( let custom_scalars_module = attributes::extract_attr(input, "custom_scalars_module").ok(); let extern_enums = attributes::extract_attr_list(input, "extern_enums").ok(); let fragments_other_variant: bool = attributes::extract_fragments_other_variant(input); + let skip_none: bool = attributes::extract_skip_none(input); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive); options.set_query_file(query_path); options.set_fragments_other_variant(fragments_other_variant); + options.set_skip_none(skip_none); if let Some(variables_derives) = variables_derives { options.set_variables_derives(variables_derives); From 39772fdb4d51af6654ae5bbe32ccf9c6e98c9c57 Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Wed, 17 Aug 2022 15:08:25 +0300 Subject: [PATCH 2/7] Finish implementing skip_none --- graphql_client_codegen/src/codegen/inputs.rs | 31 +++++++------- .../src/codegen/selection.rs | 13 ++++++ graphql_client_codegen/src/tests/mod.rs | 42 +++++++++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs index 9c943dd9..9dc0e9ae 100644 --- a/graphql_client_codegen/src/codegen/inputs.rs +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -3,6 +3,7 @@ use crate::{ codegen_options::GraphQLClientCodegenOptions, query::{BoundQuery, UsedTypes}, schema::input_is_recursive_without_indirection, + type_qualifiers::GraphqlTypeQualifier, }; use heck::ToSnakeCase; use proc_macro2::{Ident, Span, TokenStream}; @@ -28,6 +29,11 @@ pub(super) fn generate_input_object_definitions( let normalized_field_type_name = options .normalization() .field_type(field_type.id.name(query.schema)); + let optional_skip_none = if *options.skip_none() && field_type.is_optional() { + Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) + } else { + None + }; let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); let field_type_tokens = super::decorate_type(&type_name, &field_type.qualifiers); let field_type = if field_type @@ -40,23 +46,18 @@ pub(super) fn generate_input_object_definitions( } else { field_type_tokens }; - quote!(#annotation pub #name_ident: #field_type) + + quote!( + #optional_skip_none + #annotation pub #name_ident: #field_type + ) }); - match *options.skip_none() { - true => quote! { - #[serde(skip_serializing_if = "Option::is_none")] - #variable_derives - pub struct #struct_name{ - #(#fields,)* - } - }, - false => quote! { - #variable_derives - pub struct #struct_name{ - #(#fields,)* - } - }, + quote! { + #variable_derives + pub struct #struct_name{ + #(#fields,)* + } } }) .collect() diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs index 7e5b3116..1e3818d8 100644 --- a/graphql_client_codegen/src/codegen/selection.rs +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -405,6 +405,18 @@ impl<'a> ExpandedField<'a> { qualified_type }; + let optional_skip_none = if *options.skip_none() + && self + .field_type_qualifiers + .get(0) + .map(|qualifier| !qualifier.is_required()) + .unwrap_or(false) + { + Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) + } else { + None + }; + let optional_rename = self .graphql_name .as_ref() @@ -427,6 +439,7 @@ impl<'a> ExpandedField<'a> { }; let tokens = quote! { + #optional_skip_none #optional_flatten #optional_rename #optional_deprecation_annotation diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index fdc331e0..fe1df0df 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -117,3 +117,45 @@ fn fragments_other_variant_false_should_not_generate_unknown_other_variant() { }; } } + +#[test] +fn skip_none_should_generate_serde_skip_serializing() { + let query_string = include_str!("keywords_query.graphql"); + let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse keywords query"); + let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql")) + .expect("Parse keywords schema") + .into_static(); + let schema = Schema::from(schema); + + let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); + + options.set_skip_none(true); + + let query = crate::query::resolve(&schema, &query).unwrap(); + + for (_id, operation) in query.operations() { + let generated_tokens = generated_module::GeneratedModule { + query_string, + schema: &schema, + operation: &operation.name, + resolved_query: &query, + options: &options, + } + .to_token_stream() + .expect("Generate keywords module"); + + let generated_code = generated_tokens.to_string(); + + let r: syn::parse::Result = syn::parse2(generated_tokens); + + match r { + Ok(_) => { + println!("{}", generated_code); + assert!(generated_code.contains("skip_serializing_if")); + } + Err(e) => { + panic!("Error: {}\n Generated content: {}\n", e, &generated_code); + } + }; + } +} From bc5e9f127ff182f8cecbc7334ac8d70caa57baa7 Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Wed, 17 Aug 2022 15:20:25 +0300 Subject: [PATCH 3/7] Finished implementing skip_serializing_none feature --- graphql_client_codegen/src/codegen/inputs.rs | 4 +-- .../src/codegen/selection.rs | 4 +-- graphql_client_codegen/src/codegen_options.rs | 12 ++++----- graphql_client_codegen/src/tests/mod.rs | 4 +-- graphql_query_derive/src/attributes.rs | 26 +++++++++---------- graphql_query_derive/src/lib.rs | 4 +-- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs index 9dc0e9ae..fbb6dd4b 100644 --- a/graphql_client_codegen/src/codegen/inputs.rs +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -29,7 +29,7 @@ pub(super) fn generate_input_object_definitions( let normalized_field_type_name = options .normalization() .field_type(field_type.id.name(query.schema)); - let optional_skip_none = if *options.skip_none() && field_type.is_optional() { + let optional_skip_serializing_none = if *options.skip_serializing_none() && field_type.is_optional() { Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) } else { None @@ -48,7 +48,7 @@ pub(super) fn generate_input_object_definitions( }; quote!( - #optional_skip_none + #optional_skip_serializing_none #annotation pub #name_ident: #field_type ) }); diff --git a/graphql_client_codegen/src/codegen/selection.rs b/graphql_client_codegen/src/codegen/selection.rs index 1e3818d8..04dbb4f4 100644 --- a/graphql_client_codegen/src/codegen/selection.rs +++ b/graphql_client_codegen/src/codegen/selection.rs @@ -405,7 +405,7 @@ impl<'a> ExpandedField<'a> { qualified_type }; - let optional_skip_none = if *options.skip_none() + let optional_skip_serializing_none = if *options.skip_serializing_none() && self .field_type_qualifiers .get(0) @@ -439,7 +439,7 @@ impl<'a> ExpandedField<'a> { }; let tokens = quote! { - #optional_skip_none + #optional_skip_serializing_none #optional_flatten #optional_rename #optional_deprecation_annotation diff --git a/graphql_client_codegen/src/codegen_options.rs b/graphql_client_codegen/src/codegen_options.rs index 4f9bda8a..164b0dd7 100644 --- a/graphql_client_codegen/src/codegen_options.rs +++ b/graphql_client_codegen/src/codegen_options.rs @@ -46,7 +46,7 @@ pub struct GraphQLClientCodegenOptions { /// Flag to trigger generation of Other variant for fragments Enum fragments_other_variant: bool, /// Skip Serialization of None values. - skip_none: bool, + skip_serializing_none: bool, } impl GraphQLClientCodegenOptions { @@ -67,7 +67,7 @@ impl GraphQLClientCodegenOptions { custom_scalars_module: Default::default(), extern_enums: Default::default(), fragments_other_variant: Default::default(), - skip_none: Default::default(), + skip_serializing_none: Default::default(), } } @@ -219,12 +219,12 @@ impl GraphQLClientCodegenOptions { } /// Set the graphql client codegen option's skip none value. - pub fn set_skip_none(&mut self, skip_none: bool) { - self.skip_none = skip_none + pub fn set_skip_serializing_none(&mut self, skip_serializing_none: bool) { + self.skip_serializing_none = skip_serializing_none } /// Get a reference to the graphql client codegen option's skip none value. - pub fn skip_none(&self) -> &bool { - &self.skip_none + pub fn skip_serializing_none(&self) -> &bool { + &self.skip_serializing_none } } diff --git a/graphql_client_codegen/src/tests/mod.rs b/graphql_client_codegen/src/tests/mod.rs index fe1df0df..b80ba5d2 100644 --- a/graphql_client_codegen/src/tests/mod.rs +++ b/graphql_client_codegen/src/tests/mod.rs @@ -119,7 +119,7 @@ fn fragments_other_variant_false_should_not_generate_unknown_other_variant() { } #[test] -fn skip_none_should_generate_serde_skip_serializing() { +fn skip_serializing_none_should_generate_serde_skip_serializing() { let query_string = include_str!("keywords_query.graphql"); let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse keywords query"); let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql")) @@ -129,7 +129,7 @@ fn skip_none_should_generate_serde_skip_serializing() { let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli); - options.set_skip_none(true); + options.set_skip_serializing_none(true); let query = crate::query::resolve(&schema, &query).unwrap(); diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index 5ff10fb6..4382e7bb 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -103,8 +103,8 @@ pub fn extract_fragments_other_variant(ast: &syn::DeriveInput) -> bool { .unwrap_or(false) } -pub fn extract_skip_none(ast: &syn::DeriveInput) -> bool { - extract_attr(ast, "skip_none") +pub fn extract_skip_serializing_none(ast: &syn::DeriveInput) -> bool { + extract_attr(ast, "skip_serializing_none") .ok() .and_then(|s| FromStr::from_str(s.as_str()).ok()) .unwrap_or(false) @@ -228,52 +228,52 @@ mod test { } #[test] - fn test_skip_none_set_to_true() { + fn test_skip_serializing_none_set_to_true() { let input = r#" #[derive(GraphQLQuery)] #[graphql( schema_path = "x", query_path = "x", - skip_none = "true" + skip_serializing_none = "true" )] struct MyQuery; "#; let parsed = syn::parse_str(input).unwrap(); - assert!(extract_skip_none(&parsed)); + assert!(extract_skip_serializing_none(&parsed)); } #[test] - fn test_skip_none_set_to_false() { + fn test_skip_serializing_none_set_to_false() { let input = r#" #[derive(GraphQLQuery)] #[graphql( schema_path = "x", query_path = "x", - skip_none = "false" + skip_serializing_none = "false" )] struct MyQuery; "#; let parsed = syn::parse_str(input).unwrap(); - assert!(!extract_skip_none(&parsed)); + assert!(!extract_skip_serializing_none(&parsed)); } #[test] - fn test_skip_none_set_to_invalid() { + fn test_skip_serializing_none_set_to_invalid() { let input = r#" #[derive(GraphQLQuery)] #[graphql( schema_path = "x", query_path = "x", - skip_none = "invalid" + skip_serializing_none = "invalid" )] struct MyQuery; "#; let parsed = syn::parse_str(input).unwrap(); - assert!(!extract_skip_none(&parsed)); + assert!(!extract_skip_serializing_none(&parsed)); } #[test] - fn test_skip_none_unset() { + fn test_skip_serializing_none_unset() { let input = r#" #[derive(GraphQLQuery)] #[graphql( @@ -283,6 +283,6 @@ mod test { struct MyQuery; "#; let parsed = syn::parse_str(input).unwrap(); - assert!(!extract_skip_none(&parsed)); + assert!(!extract_skip_serializing_none(&parsed)); } } diff --git a/graphql_query_derive/src/lib.rs b/graphql_query_derive/src/lib.rs index b653aab9..f335aa79 100644 --- a/graphql_query_derive/src/lib.rs +++ b/graphql_query_derive/src/lib.rs @@ -64,12 +64,12 @@ fn build_graphql_client_derive_options( let custom_scalars_module = attributes::extract_attr(input, "custom_scalars_module").ok(); let extern_enums = attributes::extract_attr_list(input, "extern_enums").ok(); let fragments_other_variant: bool = attributes::extract_fragments_other_variant(input); - let skip_none: bool = attributes::extract_skip_none(input); + let skip_serializing_none: bool = attributes::extract_skip_serializing_none(input); let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive); options.set_query_file(query_path); options.set_fragments_other_variant(fragments_other_variant); - options.set_skip_none(skip_none); + options.set_skip_serializing_none(skip_serializing_none); if let Some(variables_derives) = variables_derives { options.set_variables_derives(variables_derives); From 942373b8b5996bbc5fffeff29f35a49fd20ca5a3 Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Wed, 17 Aug 2022 15:25:57 +0300 Subject: [PATCH 4/7] Update README.md --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 9cab7b82..d44dee5b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ A typed GraphQL client library for Rust. - Supports setting GraphQL fields as deprecated and having the Rust compiler check their use. - Optional reqwest-based client for boilerplate-free API calls from browsers. +- Implicit and explicit null support. ## Getting started @@ -107,6 +108,21 @@ use graphql_client::GraphQLQuery; )] struct UnionQuery; ``` +## Implicit Null + +The generated code will skip the serialization of `None` values. By default it's `false`. + +```rust +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/unions/union_schema.graphql", + query_path = "tests/unions/union_query.graphql", + skip_serializing_none = "true" +)] +struct UnionQuery; +``` ## Custom scalars From ed98d33fcdbcf11fa36bc79dc93d8024f3fa7b87 Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Fri, 19 Aug 2022 18:04:00 +0300 Subject: [PATCH 5/7] Don't require skip_serializing_none to have any attribute set and add end-to-end test for it. --- graphql_client/tests/skip_serializing_none.rs | 29 +++++++++ .../tests/skip_serializing_none/query.graphql | 6 ++ .../skip_serializing_none/schema.graphql | 25 +++++++ graphql_client_codegen/src/codegen/inputs.rs | 12 ++-- graphql_query_derive/src/attributes.rs | 65 +++++++++---------- 5 files changed, 95 insertions(+), 42 deletions(-) create mode 100644 graphql_client/tests/skip_serializing_none.rs create mode 100644 graphql_client/tests/skip_serializing_none/query.graphql create mode 100644 graphql_client/tests/skip_serializing_none/schema.graphql diff --git a/graphql_client/tests/skip_serializing_none.rs b/graphql_client/tests/skip_serializing_none.rs new file mode 100644 index 00000000..0d261847 --- /dev/null +++ b/graphql_client/tests/skip_serializing_none.rs @@ -0,0 +1,29 @@ +use graphql_client::*; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "tests/skip_serializing_none/schema.graphql", + query_path = "tests/skip_serializing_none/query.graphql", + skip_serializing_none +)] +pub struct SkipSerializingNoneMutation; + +#[test] +fn skip_serializing_none() { + use skip_serializing_none_mutation::*; + + let query = SkipSerializingNoneMutation::build_query(Variables { + param: Some(Param { + data: Author { + name: "test".to_owned(), + id: None, + }, + }), + }); + + let stringified = serde_json::to_string(&query).expect("SkipSerializingNoneMutation is valid"); + + println!("{}", stringified); + + assert!(stringified.contains(r#""data":{"name":"test"}"#)); +} diff --git a/graphql_client/tests/skip_serializing_none/query.graphql b/graphql_client/tests/skip_serializing_none/query.graphql new file mode 100644 index 00000000..028ae768 --- /dev/null +++ b/graphql_client/tests/skip_serializing_none/query.graphql @@ -0,0 +1,6 @@ +mutation SkipSerializingNoneMutation($param: Param) { + optInput(query: $param) { + name + __typename + } +} diff --git a/graphql_client/tests/skip_serializing_none/schema.graphql b/graphql_client/tests/skip_serializing_none/schema.graphql new file mode 100644 index 00000000..6f98f5c8 --- /dev/null +++ b/graphql_client/tests/skip_serializing_none/schema.graphql @@ -0,0 +1,25 @@ +schema { + mutation: Mutation +} + +# The query type, represents all of the entry points into our object graph +type Mutation { + optInput(mutation: Param!): Named +} + +input Param { + data: Author! +} + +input Author { + id: String, + name: String! +} + +# A named entity +type Named { + # The ID of the entity + id: ID! + # The name of the entity + name: String! +} diff --git a/graphql_client_codegen/src/codegen/inputs.rs b/graphql_client_codegen/src/codegen/inputs.rs index fbb6dd4b..bc2ee5cc 100644 --- a/graphql_client_codegen/src/codegen/inputs.rs +++ b/graphql_client_codegen/src/codegen/inputs.rs @@ -3,7 +3,6 @@ use crate::{ codegen_options::GraphQLClientCodegenOptions, query::{BoundQuery, UsedTypes}, schema::input_is_recursive_without_indirection, - type_qualifiers::GraphqlTypeQualifier, }; use heck::ToSnakeCase; use proc_macro2::{Ident, Span, TokenStream}; @@ -29,11 +28,12 @@ pub(super) fn generate_input_object_definitions( let normalized_field_type_name = options .normalization() .field_type(field_type.id.name(query.schema)); - let optional_skip_serializing_none = if *options.skip_serializing_none() && field_type.is_optional() { - Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) - } else { - None - }; + let optional_skip_serializing_none = + if *options.skip_serializing_none() && field_type.is_optional() { + Some(quote!(#[serde(skip_serializing_if = "Option::is_none")])) + } else { + None + }; let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site()); let field_type_tokens = super::decorate_type(&type_name, &field_type.qualifiers); let field_type = if field_type diff --git a/graphql_query_derive/src/attributes.rs b/graphql_query_derive/src/attributes.rs index 4382e7bb..32f6865e 100644 --- a/graphql_query_derive/src/attributes.rs +++ b/graphql_query_derive/src/attributes.rs @@ -11,6 +11,32 @@ fn path_to_match() -> syn::Path { syn::parse_str("graphql").expect("`graphql` is a valid path") } +pub fn ident_exists(ast: &syn::DeriveInput, ident: &str) -> Result<(), syn::Error> { + let graphql_path = path_to_match(); + let attribute = ast + .attrs + .iter() + .find(|attr| attr.path == graphql_path) + .ok_or_else(|| syn::Error::new_spanned(ast, "The graphql attribute is missing"))?; + + if let syn::Meta::List(items) = &attribute.parse_meta().expect("Attribute is well formatted") { + for item in items.nested.iter() { + if let syn::NestedMeta::Meta(syn::Meta::Path(path)) = item { + if let Some(ident_) = path.get_ident() { + if ident_ == ident { + return Ok(()); + } + } + } + } + } + + Err(syn::Error::new_spanned( + &ast, + format!("Ident `{}` not found", ident), + )) +} + /// Extract an configuration parameter specified in the `graphql` attribute. pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result { let attributes = &ast.attrs; @@ -104,10 +130,7 @@ pub fn extract_fragments_other_variant(ast: &syn::DeriveInput) -> bool { } pub fn extract_skip_serializing_none(ast: &syn::DeriveInput) -> bool { - extract_attr(ast, "skip_serializing_none") - .ok() - .and_then(|s| FromStr::from_str(s.as_str()).ok()) - .unwrap_or(false) + ident_exists(ast, "skip_serializing_none").is_ok() } #[cfg(test)] @@ -228,13 +251,13 @@ mod test { } #[test] - fn test_skip_serializing_none_set_to_true() { + fn test_skip_serializing_none_set() { let input = r#" #[derive(GraphQLQuery)] #[graphql( schema_path = "x", query_path = "x", - skip_serializing_none = "true" + skip_serializing_none )] struct MyQuery; "#; @@ -242,36 +265,6 @@ mod test { assert!(extract_skip_serializing_none(&parsed)); } - #[test] - fn test_skip_serializing_none_set_to_false() { - let input = r#" - #[derive(GraphQLQuery)] - #[graphql( - schema_path = "x", - query_path = "x", - skip_serializing_none = "false" - )] - struct MyQuery; - "#; - let parsed = syn::parse_str(input).unwrap(); - assert!(!extract_skip_serializing_none(&parsed)); - } - - #[test] - fn test_skip_serializing_none_set_to_invalid() { - let input = r#" - #[derive(GraphQLQuery)] - #[graphql( - schema_path = "x", - query_path = "x", - skip_serializing_none = "invalid" - )] - struct MyQuery; - "#; - let parsed = syn::parse_str(input).unwrap(); - assert!(!extract_skip_serializing_none(&parsed)); - } - #[test] fn test_skip_serializing_none_unset() { let input = r#" From 507974274957f58687c67524682fb9ecc510d3b7 Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Wed, 7 Sep 2022 06:35:47 +0300 Subject: [PATCH 6/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d44dee5b..8d17970d 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ use graphql_client::GraphQLQuery; #[graphql( schema_path = "tests/unions/union_schema.graphql", query_path = "tests/unions/union_query.graphql", - skip_serializing_none = "true" + skip_serializing_none )] struct UnionQuery; ``` From 612a52243ca1438b64a85b67d7633fb5823244ce Mon Sep 17 00:00:00 2001 From: Selyatin Ismet Date: Thu, 8 Sep 2022 12:02:53 +0300 Subject: [PATCH 7/7] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d17970d..6d5ba64a 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ struct UnionQuery; ``` ## Implicit Null -The generated code will skip the serialization of `None` values. By default it's `false`. +The generated code will skip the serialization of `None` values. ```rust use graphql_client::GraphQLQuery;