Skip to content

Commit

Permalink
feat: add QueryVariableLiterals trait
Browse files Browse the repository at this point in the history
  • Loading branch information
obmarg committed Aug 23, 2024
1 parent 460969c commit 3b97142
Show file tree
Hide file tree
Showing 12 changed files with 264 additions and 15 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html

## Unreleased - xxxx-xx-xx

### Bug Fixes

- Tidied up the output of object & list literals in the clients GraphQl output.

## v3.7.3 - 2024-06-04

### Changes
Expand Down
1 change: 1 addition & 0 deletions cynic-codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod fragment_derive;
pub mod generics_for_serde;
pub mod inline_fragments_derive;
pub mod input_object_derive;
pub mod query_variable_literals_derive;
pub mod query_variables_derive;
pub mod registration;
pub mod scalar_derive;
Expand Down
45 changes: 45 additions & 0 deletions cynic-codegen/src/query_variable_literals_derive/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use darling::util::SpannedValue;
use syn::spanned::Spanned;

use crate::{idents::RenamableFieldIdent, RenameAll};

#[derive(darling::FromDeriveInput)]
#[darling(attributes(cynic), supports(struct_named))]
pub struct QueryVariableLiteralsInput {
pub(super) ident: proc_macro2::Ident,
pub(super) generics: syn::Generics,
pub(super) data: darling::ast::Data<(), QueryVariableLiteralsField>,

#[darling(default)]
pub(super) rename_all: Option<RenameAll>,
}

#[derive(Debug, darling::FromField)]
#[darling(attributes(cynic))]
pub(super) struct QueryVariableLiteralsField {
pub(super) ident: Option<proc_macro2::Ident>,

#[darling(default)]
pub(super) skip_serializing_if: Option<SpannedValue<syn::Path>>,

#[darling(default)]
pub(super) rename: Option<SpannedValue<String>>,
}

impl QueryVariableLiteralsField {
pub fn graphql_ident(&self, rename_all: Option<RenameAll>) -> RenamableFieldIdent {
let mut ident = RenamableFieldIdent::from(
self.ident
.clone()
.expect("InputObject only supports named structs"),
);
if let Some(rename) = &self.rename {
let span = rename.span();
let rename = (**rename).clone();
ident.set_rename(rename, span)
} else if let Some(rename_all) = rename_all {
ident.rename_with(rename_all, self.ident.span())
}
ident
}
}
79 changes: 79 additions & 0 deletions cynic-codegen/src/query_variable_literals_derive/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use {
proc_macro2::TokenStream,
quote::{format_ident, quote},
syn::visit_mut::{self, VisitMut},
};

mod input;

use crate::generics_for_serde;

use self::input::QueryVariableLiteralsInput;

pub fn query_variable_literals_derive(ast: &syn::DeriveInput) -> Result<TokenStream, syn::Error> {
use darling::FromDeriveInput;

match QueryVariableLiteralsInput::from_derive_input(ast) {
Ok(input) => inlineable_variables_derive_impl(input),
Err(e) => Ok(e.write_errors()),
}
}

pub fn inlineable_variables_derive_impl(
input: QueryVariableLiteralsInput,
) -> Result<TokenStream, syn::Error> {
let ident = &input.ident;

let (_, ty_generics, _) = input.generics.split_for_impl();
let generics_with_ser = generics_for_serde::with_serialize_bounds(&input.generics);
let (impl_generics_with_ser, _, where_clause_with_ser) = generics_with_ser.split_for_impl();

let input_fields = input.data.take_struct().unwrap().fields;

let mut match_arms = Vec::new();

for f in input_fields {
let name = f.ident.as_ref().unwrap();

let name_str =
proc_macro2::Literal::string(&f.graphql_ident(input.rename_all).graphql_name());

let mut match_arm_rhs =
quote! { Some(cynic::queries::to_input_literal(&self.#name).ok()?) };

if let Some(skip_check_fn) = f.skip_serializing_if {
let skip_check_fn = &*skip_check_fn;
match_arm_rhs = quote! {
if #skip_check_fn(&self.#name) {
None
} else {
#match_arm_rhs
}
}
}

match_arms.push(quote! {
#name_str => #match_arm_rhs,
})
}

Ok(quote! {
#[automatically_derived]
impl #impl_generics_with_ser cynic::QueryVariableLiterals for #ident #ty_generics #where_clause_with_ser {
fn get(&self, variable_name: &str) -> Option<cynic::queries::InputLiteral> {
match variable_name {
#(#match_arms)*
_ => None
}
}
}
})
}

struct TurnLifetimesToStatic;
impl VisitMut for TurnLifetimesToStatic {
fn visit_lifetime_mut(&mut self, i: &mut syn::Lifetime) {
i.ident = format_ident!("static");
visit_mut::visit_lifetime_mut(self, i)
}
}
16 changes: 15 additions & 1 deletion cynic-proc-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use proc_macro::TokenStream;

use cynic_codegen::{
enum_derive, fragment_derive, inline_fragments_derive, input_object_derive,
query_variables_derive, scalar_derive, schema_for_derives, schema_module_attr, use_schema,
query_variable_literals_derive, query_variables_derive, scalar_derive, schema_for_derives,
schema_module_attr, use_schema,
};

/// Imports a schema for use by cynic.
Expand Down Expand Up @@ -67,6 +68,19 @@ pub fn query_variables_derive(input: TokenStream) -> TokenStream {
rv
}

/// Derives `cynic::QueryVariableLiterals`
#[proc_macro_derive(QueryVariableLiterals, attributes(cynic))]
pub fn query_variable_literals_derive(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input as syn::DeriveInput);

let rv = match query_variable_literals_derive::query_variable_literals_derive(&ast) {
Ok(tokens) => tokens.into(),
Err(e) => e.to_compile_error().into(),
};

rv
}

/// Derives `cynic::InlineFragments`
///
/// See [the book for usage details](https://cynic-rs.dev/derives/inline-fragments.html)
Expand Down
4 changes: 2 additions & 2 deletions cynic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,12 @@ pub use {
id::Id,
operation::{Operation, OperationBuildError, OperationBuilder, StreamingOperation},
result::*,
variables::{QueryVariables, QueryVariablesFields},
variables::{QueryVariableLiterals, QueryVariables, QueryVariablesFields},
};

pub use cynic_proc_macros::{
schema, schema_for_derives, use_schema, Enum, InlineFragments, InputObject, QueryFragment,
QueryVariables, Scalar,
QueryVariableLiterals, QueryVariables, Scalar,
};

pub use static_assertions::assert_type_eq_all;
Expand Down
14 changes: 10 additions & 4 deletions cynic/src/queries/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,21 @@ impl std::fmt::Display for InputLiteral {
InputLiteral::Id(val) => write!(f, "\"{}\"", val),
InputLiteral::Object(fields) => {
write!(f, "{{")?;
for field in fields {
write!(f, "{}: {}, ", field.name, field.value)?;
for (i, field) in fields.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "{}: {}", field.name, field.value)?;
}
write!(f, "}}")
}
InputLiteral::List(vals) => {
write!(f, "[")?;
for val in vals {
write!(f, "{}, ", val)?;
for (i, val) in vals.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "{}", val)?;
}
write!(f, "]")
}
Expand Down
27 changes: 27 additions & 0 deletions cynic/src/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
use std::marker::PhantomData;

use crate::queries::InputLiteral;

/// The type of a variable
#[derive(Debug, Clone, Copy)]
pub enum VariableType {
Expand Down Expand Up @@ -40,6 +42,16 @@ impl QueryVariables for () {

impl QueryVariablesFields for () {}

/// Allows a query variable struct to be converted to literals for easier inlining
/// into a graphql document.
///
/// Cynic can derive this automatically or you can add it to a QueryVariables struct
/// yourself.
pub trait QueryVariableLiterals {
/// Gets an InputLiteral for the given variable from this set of variables
fn get(&self, variable_name: &str) -> Option<InputLiteral>;
}

#[doc(hidden)]
/// A VariableDefinition.
///
Expand All @@ -59,3 +71,18 @@ impl<Variables, Type> VariableDefinition<Variables, Type> {
}
}
}

#[cfg(test)]
mod tests {
use cynic::QueryVariableLiterals;

#[test]
fn query_variable_literals_is_object_safe() {
#[derive(QueryVariableLiterals)]
struct Blah {
x: String,
}

let _: Box<dyn QueryVariableLiterals> = Box::new(Blah { x: "hello".into() });
}
}
3 changes: 1 addition & 2 deletions cynic/tests/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ fn test_literal_object_inside_list() {

insta::assert_display_snapshot!(query.query, @r###"
query Query {
filteredPosts(filters: {any: [{states: [DRAFT, ], }, ], }) {
filteredPosts(filters: {any: [{states: [DRAFT]}]}) {
hasMetadata
}
}
"###);
}

Expand Down
6 changes: 2 additions & 4 deletions cynic/tests/enum-arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ fn test_enum_argument_literal() {

insta::assert_display_snapshot!(query.query, @r###"
query Query {
filteredPosts(filters: {states: [DRAFT, ], }) {
filteredPosts(filters: {states: [DRAFT]}) {
hasMetadata
}
}
"###);
}

Expand Down Expand Up @@ -65,11 +64,10 @@ fn test_enum_argument() {

insta::assert_display_snapshot!(query.query, @r###"
query Query {
filteredPosts(filters: {states: [POSTED, ], }) {
filteredPosts(filters: {states: [POSTED]}) {
hasMetadata
}
}
"###);
}

Expand Down
77 changes: 77 additions & 0 deletions cynic/tests/variable-inlining.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use cynic::QueryVariableLiterals;

#[derive(QueryVariableLiterals)]
struct TestArgs<'a> {
#[cynic(skip_serializing_if = "Option::is_none")]
a_str: Option<&'a str>,
a_bool: Option<bool>,
an_input: AnInputType,
}

#[derive(cynic::InputObject)]
#[cynic(schema_path = "../schemas/simple.graphql")]
struct AnInputType {
favourite_dessert: Option<Dessert>,
}

#[derive(cynic::Enum)]
#[cynic(schema_path = "../schemas/simple.graphql")]
enum Dessert {
Cheesecake,
IceCream,
}

#[test]
fn test_the_derive() {
let args = TestArgs {
a_str: Some("hello"),
a_bool: Some(false),
an_input: AnInputType {
favourite_dessert: Some(Dessert::Cheesecake),
},
};

assert_eq!(args.get("aStr").unwrap().to_string(), "\"hello\"");

assert_eq!(args.get("aBool").unwrap().to_string(), "false");

assert_eq!(
args.get("anInput").unwrap().to_string(),
"{favouriteDessert: CHEESECAKE}"
);
}

#[test]
fn test_the_derive_with_skip_serializing_if() {
let args = TestArgs {
a_str: None,
a_bool: None,
an_input: AnInputType {
favourite_dessert: None,
},
};

assert_eq!(args.get("aStr"), None)
}

#[test]
fn test_derive_with_renames() {
#[derive(QueryVariableLiterals)]
#[cynic(rename_all = "SCREAMING_SNAKE_CASE")]
struct TestArgs<'a> {
a_str: Option<&'a str>,
#[cynic(rename = "renamedThisYeah")]
a_bool: Option<bool>,
}
let args = TestArgs {
a_str: Some("hello"),
a_bool: Some(true),
};

assert_eq!(args.get("A_STR").unwrap().to_string(), "\"hello\"");
assert_eq!(args.get("renamedThisYeah").unwrap().to_string(), "true");
}

mod schema {
cynic::use_schema!("../schemas/simple.graphql");
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source: examples/examples/github-mutation.rs
expression: query.query
---
mutation CommentOnMutationSupportIssue($commentBody: String!) {
addComment(input: {body: $commentBody, subjectId: "MDU6SXNzdWU2ODU4NzUxMzQ=", clientMutationId: null, }) {
addComment(input: {body: $commentBody, subjectId: "MDU6SXNzdWU2ODU4NzUxMzQ=", clientMutationId: null}) {
commentEdge {
node {
id
Expand All @@ -12,4 +12,3 @@ mutation CommentOnMutationSupportIssue($commentBody: String!) {
}
}


0 comments on commit 3b97142

Please sign in to comment.