Skip to content

Commit

Permalink
Merge pull request graphql-rust#431 from Selyatin/skip_none
Browse files Browse the repository at this point in the history
  • Loading branch information
tomhoule authored Sep 8, 2022
2 parents 45faf4a + 612a522 commit 0776197
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 2 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -107,6 +108,21 @@ use graphql_client::GraphQLQuery;
)]
struct UnionQuery;
```
## Implicit Null

The generated code will skip the serialization of `None` values.

```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
)]
struct UnionQuery;
```

## Custom scalars

Expand Down
29 changes: 29 additions & 0 deletions graphql_client/tests/skip_serializing_none.rs
Original file line number Diff line number Diff line change
@@ -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"}"#));
}
6 changes: 6 additions & 0 deletions graphql_client/tests/skip_serializing_none/query.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mutation SkipSerializingNoneMutation($param: Param) {
optInput(query: $param) {
name
__typename
}
}
25 changes: 25 additions & 0 deletions graphql_client/tests/skip_serializing_none/schema.graphql
Original file line number Diff line number Diff line change
@@ -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!
}
14 changes: 12 additions & 2 deletions graphql_client_codegen/src/codegen/inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +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 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
Expand All @@ -40,12 +46,16 @@ pub(super) fn generate_input_object_definitions(
} else {
field_type_tokens
};
quote!(#annotation pub #name_ident: #field_type)

quote!(
#optional_skip_serializing_none
#annotation pub #name_ident: #field_type
)
});

quote! {
#variable_derives
pub struct #struct_name {
pub struct #struct_name{
#(#fields,)*
}
}
Expand Down
13 changes: 13 additions & 0 deletions graphql_client_codegen/src/codegen/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,18 @@ impl<'a> ExpandedField<'a> {
qualified_type
};

let optional_skip_serializing_none = if *options.skip_serializing_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()
Expand All @@ -427,6 +439,7 @@ impl<'a> ExpandedField<'a> {
};

let tokens = quote! {
#optional_skip_serializing_none
#optional_flatten
#optional_rename
#optional_deprecation_annotation
Expand Down
13 changes: 13 additions & 0 deletions graphql_client_codegen/src/codegen_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct GraphQLClientCodegenOptions {
extern_enums: Vec<String>,
/// Flag to trigger generation of Other variant for fragments Enum
fragments_other_variant: bool,
/// Skip Serialization of None values.
skip_serializing_none: bool,
}

impl GraphQLClientCodegenOptions {
Expand All @@ -65,6 +67,7 @@ impl GraphQLClientCodegenOptions {
custom_scalars_module: Default::default(),
extern_enums: Default::default(),
fragments_other_variant: Default::default(),
skip_serializing_none: Default::default(),
}
}

Expand Down Expand Up @@ -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_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_serializing_none(&self) -> &bool {
&self.skip_serializing_none
}
}
42 changes: 42 additions & 0 deletions graphql_client_codegen/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,45 @@ fn fragments_other_variant_false_should_not_generate_unknown_other_variant() {
};
}
}

#[test]
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"))
.expect("Parse keywords schema")
.into_static();
let schema = Schema::from(schema);

let mut options = GraphQLClientCodegenOptions::new(CodegenMode::Cli);

options.set_skip_serializing_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<proc_macro2::TokenStream> = 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);
}
};
}
}
59 changes: 59 additions & 0 deletions graphql_query_derive/src/attributes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, syn::Error> {
let attributes = &ast.attrs;
Expand Down Expand Up @@ -103,6 +129,10 @@ pub fn extract_fragments_other_variant(ast: &syn::DeriveInput) -> bool {
.unwrap_or(false)
}

pub fn extract_skip_serializing_none(ast: &syn::DeriveInput) -> bool {
ident_exists(ast, "skip_serializing_none").is_ok()
}

#[cfg(test)]
mod test {
use super::*;
Expand Down Expand Up @@ -219,4 +249,33 @@ mod test {
let parsed = syn::parse_str(input).unwrap();
assert!(!extract_fragments_other_variant(&parsed));
}

#[test]
fn test_skip_serializing_none_set() {
let input = r#"
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "x",
query_path = "x",
skip_serializing_none
)]
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#"
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "x",
query_path = "x",
)]
struct MyQuery;
"#;
let parsed = syn::parse_str(input).unwrap();
assert!(!extract_skip_serializing_none(&parsed));
}
}
2 changes: 2 additions & 0 deletions graphql_query_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_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_serializing_none(skip_serializing_none);

if let Some(variables_derives) = variables_derives {
options.set_variables_derives(variables_derives);
Expand Down

0 comments on commit 0776197

Please sign in to comment.