Skip to content

Commit 0776197

Browse files
authored
Merge pull request #431 from Selyatin/skip_none
2 parents 45faf4a + 612a522 commit 0776197

File tree

10 files changed

+217
-2
lines changed

10 files changed

+217
-2
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ A typed GraphQL client library for Rust.
1919
- Supports setting GraphQL fields as deprecated and having the Rust compiler check
2020
their use.
2121
- Optional reqwest-based client for boilerplate-free API calls from browsers.
22+
- Implicit and explicit null support.
2223

2324
## Getting started
2425

@@ -107,6 +108,21 @@ use graphql_client::GraphQLQuery;
107108
)]
108109
struct UnionQuery;
109110
```
111+
## Implicit Null
112+
113+
The generated code will skip the serialization of `None` values.
114+
115+
```rust
116+
use graphql_client::GraphQLQuery;
117+
118+
#[derive(GraphQLQuery)]
119+
#[graphql(
120+
schema_path = "tests/unions/union_schema.graphql",
121+
query_path = "tests/unions/union_query.graphql",
122+
skip_serializing_none
123+
)]
124+
struct UnionQuery;
125+
```
110126

111127
## Custom scalars
112128

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
use graphql_client::*;
2+
3+
#[derive(GraphQLQuery)]
4+
#[graphql(
5+
schema_path = "tests/skip_serializing_none/schema.graphql",
6+
query_path = "tests/skip_serializing_none/query.graphql",
7+
skip_serializing_none
8+
)]
9+
pub struct SkipSerializingNoneMutation;
10+
11+
#[test]
12+
fn skip_serializing_none() {
13+
use skip_serializing_none_mutation::*;
14+
15+
let query = SkipSerializingNoneMutation::build_query(Variables {
16+
param: Some(Param {
17+
data: Author {
18+
name: "test".to_owned(),
19+
id: None,
20+
},
21+
}),
22+
});
23+
24+
let stringified = serde_json::to_string(&query).expect("SkipSerializingNoneMutation is valid");
25+
26+
println!("{}", stringified);
27+
28+
assert!(stringified.contains(r#""data":{"name":"test"}"#));
29+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
mutation SkipSerializingNoneMutation($param: Param) {
2+
optInput(query: $param) {
3+
name
4+
__typename
5+
}
6+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
schema {
2+
mutation: Mutation
3+
}
4+
5+
# The query type, represents all of the entry points into our object graph
6+
type Mutation {
7+
optInput(mutation: Param!): Named
8+
}
9+
10+
input Param {
11+
data: Author!
12+
}
13+
14+
input Author {
15+
id: String,
16+
name: String!
17+
}
18+
19+
# A named entity
20+
type Named {
21+
# The ID of the entity
22+
id: ID!
23+
# The name of the entity
24+
name: String!
25+
}

graphql_client_codegen/src/codegen/inputs.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ pub(super) fn generate_input_object_definitions(
2828
let normalized_field_type_name = options
2929
.normalization()
3030
.field_type(field_type.id.name(query.schema));
31+
let optional_skip_serializing_none =
32+
if *options.skip_serializing_none() && field_type.is_optional() {
33+
Some(quote!(#[serde(skip_serializing_if = "Option::is_none")]))
34+
} else {
35+
None
36+
};
3137
let type_name = Ident::new(normalized_field_type_name.as_ref(), Span::call_site());
3238
let field_type_tokens = super::decorate_type(&type_name, &field_type.qualifiers);
3339
let field_type = if field_type
@@ -40,12 +46,16 @@ pub(super) fn generate_input_object_definitions(
4046
} else {
4147
field_type_tokens
4248
};
43-
quote!(#annotation pub #name_ident: #field_type)
49+
50+
quote!(
51+
#optional_skip_serializing_none
52+
#annotation pub #name_ident: #field_type
53+
)
4454
});
4555

4656
quote! {
4757
#variable_derives
48-
pub struct #struct_name {
58+
pub struct #struct_name{
4959
#(#fields,)*
5060
}
5161
}

graphql_client_codegen/src/codegen/selection.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,18 @@ impl<'a> ExpandedField<'a> {
405405
qualified_type
406406
};
407407

408+
let optional_skip_serializing_none = if *options.skip_serializing_none()
409+
&& self
410+
.field_type_qualifiers
411+
.get(0)
412+
.map(|qualifier| !qualifier.is_required())
413+
.unwrap_or(false)
414+
{
415+
Some(quote!(#[serde(skip_serializing_if = "Option::is_none")]))
416+
} else {
417+
None
418+
};
419+
408420
let optional_rename = self
409421
.graphql_name
410422
.as_ref()
@@ -427,6 +439,7 @@ impl<'a> ExpandedField<'a> {
427439
};
428440

429441
let tokens = quote! {
442+
#optional_skip_serializing_none
430443
#optional_flatten
431444
#optional_rename
432445
#optional_deprecation_annotation

graphql_client_codegen/src/codegen_options.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub struct GraphQLClientCodegenOptions {
4545
extern_enums: Vec<String>,
4646
/// Flag to trigger generation of Other variant for fragments Enum
4747
fragments_other_variant: bool,
48+
/// Skip Serialization of None values.
49+
skip_serializing_none: bool,
4850
}
4951

5052
impl GraphQLClientCodegenOptions {
@@ -65,6 +67,7 @@ impl GraphQLClientCodegenOptions {
6567
custom_scalars_module: Default::default(),
6668
extern_enums: Default::default(),
6769
fragments_other_variant: Default::default(),
70+
skip_serializing_none: Default::default(),
6871
}
6972
}
7073

@@ -214,4 +217,14 @@ impl GraphQLClientCodegenOptions {
214217
pub fn fragments_other_variant(&self) -> &bool {
215218
&self.fragments_other_variant
216219
}
220+
221+
/// Set the graphql client codegen option's skip none value.
222+
pub fn set_skip_serializing_none(&mut self, skip_serializing_none: bool) {
223+
self.skip_serializing_none = skip_serializing_none
224+
}
225+
226+
/// Get a reference to the graphql client codegen option's skip none value.
227+
pub fn skip_serializing_none(&self) -> &bool {
228+
&self.skip_serializing_none
229+
}
217230
}

graphql_client_codegen/src/tests/mod.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,45 @@ fn fragments_other_variant_false_should_not_generate_unknown_other_variant() {
117117
};
118118
}
119119
}
120+
121+
#[test]
122+
fn skip_serializing_none_should_generate_serde_skip_serializing() {
123+
let query_string = include_str!("keywords_query.graphql");
124+
let query = graphql_parser::parse_query::<&str>(query_string).expect("Parse keywords query");
125+
let schema = graphql_parser::parse_schema(include_str!("keywords_schema.graphql"))
126+
.expect("Parse keywords schema")
127+
.into_static();
128+
let schema = Schema::from(schema);
129+
130+
let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli);
131+
132+
options.set_skip_serializing_none(true);
133+
134+
let query = crate::query::resolve(&schema, &query).unwrap();
135+
136+
for (_id, operation) in query.operations() {
137+
let generated_tokens = generated_module::GeneratedModule {
138+
query_string,
139+
schema: &schema,
140+
operation: &operation.name,
141+
resolved_query: &query,
142+
options: &options,
143+
}
144+
.to_token_stream()
145+
.expect("Generate keywords module");
146+
147+
let generated_code = generated_tokens.to_string();
148+
149+
let r: syn::parse::Result<proc_macro2::TokenStream> = syn::parse2(generated_tokens);
150+
151+
match r {
152+
Ok(_) => {
153+
println!("{}", generated_code);
154+
assert!(generated_code.contains("skip_serializing_if"));
155+
}
156+
Err(e) => {
157+
panic!("Error: {}\n Generated content: {}\n", e, &generated_code);
158+
}
159+
};
160+
}
161+
}

graphql_query_derive/src/attributes.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,32 @@ fn path_to_match() -> syn::Path {
1111
syn::parse_str("graphql").expect("`graphql` is a valid path")
1212
}
1313

14+
pub fn ident_exists(ast: &syn::DeriveInput, ident: &str) -> Result<(), syn::Error> {
15+
let graphql_path = path_to_match();
16+
let attribute = ast
17+
.attrs
18+
.iter()
19+
.find(|attr| attr.path == graphql_path)
20+
.ok_or_else(|| syn::Error::new_spanned(ast, "The graphql attribute is missing"))?;
21+
22+
if let syn::Meta::List(items) = &attribute.parse_meta().expect("Attribute is well formatted") {
23+
for item in items.nested.iter() {
24+
if let syn::NestedMeta::Meta(syn::Meta::Path(path)) = item {
25+
if let Some(ident_) = path.get_ident() {
26+
if ident_ == ident {
27+
return Ok(());
28+
}
29+
}
30+
}
31+
}
32+
}
33+
34+
Err(syn::Error::new_spanned(
35+
&ast,
36+
format!("Ident `{}` not found", ident),
37+
))
38+
}
39+
1440
/// Extract an configuration parameter specified in the `graphql` attribute.
1541
pub fn extract_attr(ast: &syn::DeriveInput, attr: &str) -> Result<String, syn::Error> {
1642
let attributes = &ast.attrs;
@@ -103,6 +129,10 @@ pub fn extract_fragments_other_variant(ast: &syn::DeriveInput) -> bool {
103129
.unwrap_or(false)
104130
}
105131

132+
pub fn extract_skip_serializing_none(ast: &syn::DeriveInput) -> bool {
133+
ident_exists(ast, "skip_serializing_none").is_ok()
134+
}
135+
106136
#[cfg(test)]
107137
mod test {
108138
use super::*;
@@ -219,4 +249,33 @@ mod test {
219249
let parsed = syn::parse_str(input).unwrap();
220250
assert!(!extract_fragments_other_variant(&parsed));
221251
}
252+
253+
#[test]
254+
fn test_skip_serializing_none_set() {
255+
let input = r#"
256+
#[derive(GraphQLQuery)]
257+
#[graphql(
258+
schema_path = "x",
259+
query_path = "x",
260+
skip_serializing_none
261+
)]
262+
struct MyQuery;
263+
"#;
264+
let parsed = syn::parse_str(input).unwrap();
265+
assert!(extract_skip_serializing_none(&parsed));
266+
}
267+
268+
#[test]
269+
fn test_skip_serializing_none_unset() {
270+
let input = r#"
271+
#[derive(GraphQLQuery)]
272+
#[graphql(
273+
schema_path = "x",
274+
query_path = "x",
275+
)]
276+
struct MyQuery;
277+
"#;
278+
let parsed = syn::parse_str(input).unwrap();
279+
assert!(!extract_skip_serializing_none(&parsed));
280+
}
222281
}

graphql_query_derive/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ fn build_graphql_client_derive_options(
6464
let custom_scalars_module = attributes::extract_attr(input, "custom_scalars_module").ok();
6565
let extern_enums = attributes::extract_attr_list(input, "extern_enums").ok();
6666
let fragments_other_variant: bool = attributes::extract_fragments_other_variant(input);
67+
let skip_serializing_none: bool = attributes::extract_skip_serializing_none(input);
6768

6869
let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Derive);
6970
options.set_query_file(query_path);
7071
options.set_fragments_other_variant(fragments_other_variant);
72+
options.set_skip_serializing_none(skip_serializing_none);
7173

7274
if let Some(variables_derives) = variables_derives {
7375
options.set_variables_derives(variables_derives);

0 commit comments

Comments
 (0)