Skip to content

Commit

Permalink
feat: infer bounds for generic nested query fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
obmarg committed Apr 6, 2024
1 parent a73244d commit 4713ea6
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 0 deletions.
89 changes: 89 additions & 0 deletions cynic-codegen/src/fragment_derive/fragment_impl.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use darling::usage::{GenericsExt, Purpose, UsesTypeParams};
use syn::{parse_quote, Token, WhereClause};

use {
proc_macro2::{Span, TokenStream},
quote::{quote, quote_spanned},
Expand Down Expand Up @@ -28,6 +31,7 @@ pub struct FragmentImpl<'schema, 'a> {
variables_fields: syn::Type,
graphql_type_name: String,
schema_type_path: syn::Path,
additional_where: Option<syn::WhereClause>,
}

#[allow(clippy::large_enum_variant)]
Expand Down Expand Up @@ -83,6 +87,14 @@ impl<'schema, 'a: 'schema> FragmentImpl<'schema, 'a> {
let variables_fields = variables_fields_path(variables);
let variables_fields = variables_fields.as_ref();

let additional_where = additional_where_clause(
generics,
fields,
schema,
&field_module_path,
variables_fields,
);

let selections = fields
.iter()
.map(|(field, schema_field)| {
Expand Down Expand Up @@ -111,6 +123,7 @@ impl<'schema, 'a: 'schema> FragmentImpl<'schema, 'a> {
variables_fields,
graphql_type_name: graphql_type_name.to_string(),
schema_type_path,
additional_where,
})
}
}
Expand Down Expand Up @@ -167,6 +180,64 @@ fn process_field<'a>(
}))
}

fn additional_where_clause(
generics: &syn::Generics,
fields: &[(FragmentDeriveField, Option<Field<'_>>)],
schema: &Schema<'_, Unvalidated>,
field_module_path: &syn::Path,
variables_fields: Option<&syn::Path>,
) -> Option<WhereClause> {
let all_params = generics.declared_type_params();
if all_params.is_empty() {
return None;
}
let options = Purpose::BoundImpl.into();

let mut predicates: Vec<syn::WherePredicate> = vec![];

for (field, schema_field) in fields {
let Some(schema_field) = schema_field else {
continue;
};
let inner_type = schema_field.field_type.inner_type(schema);
if !inner_type.is_composite() {
// We only care about generics on composite types.
continue;
}
if field.ty.uses_type_params(&options, &all_params).is_empty() {
// If this field uses no type params we skip it
continue;
}

let ty = &field.ty;
let marker_ty = schema_field.marker_ident().to_path(field_module_path);
predicates.push(parse_quote! {
#ty: cynic::QueryFragment<SchemaType = <#marker_ty as cynic::schema::Field>::Type>
});
match variables_fields {
Some(variables_fields) => {
predicates.push(parse_quote! {
#variables_fields: cynic::queries::VariableMatch<<#ty as cynic::QueryFragment>::VariablesFields>
});
}
None => {
predicates.push(parse_quote! {
(): cynic::queries::VariableMatch<<#ty as cynic::QueryFragment>::VariablesFields>
});
}
}
}

if predicates.is_empty() {
return None;
}

Some(WhereClause {
where_token: <Token![where]>::default(),
predicates: predicates.into_iter().collect(),
})
}

impl quote::ToTokens for FragmentImpl<'_, '_> {
fn to_tokens(&self, tokens: &mut TokenStream) {
use quote::TokenStreamExt;
Expand All @@ -179,6 +250,17 @@ impl quote::ToTokens for FragmentImpl<'_, '_> {
let fragment_name = proc_macro2::Literal::string(&target_struct.to_string());
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();

let where_clause = match (where_clause, &self.additional_where) {
(None, None) => None,
(Some(lhs), None) => Some(quote! { #lhs }),
(None, Some(rhs)) => Some(quote! { #rhs }),
(Some(lhs), Some(rhs)) => {
let mut new = lhs.clone();
new.predicates.extend(rhs.predicates.clone());
Some(quote! { #new })
}
};

tokens.append_all(quote! {
#[automatically_derived]
impl #impl_generics cynic::QueryFragment for #target_struct #ty_generics #where_clause {
Expand Down Expand Up @@ -383,6 +465,13 @@ impl quote::ToTokens for SpreadSelection {
}

impl OutputType<'_> {
fn is_composite(&self) -> bool {
matches!(
self,
OutputType::Object(_) | OutputType::Interface(_) | OutputType::Union(_)
)
}

fn as_kind(&self) -> FieldKind {
match self {
OutputType::Scalar(_) => FieldKind::Scalar,
Expand Down
12 changes: 12 additions & 0 deletions cynic-codegen/tests/snapshots/use_schema__simple.graphql.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ expression: "format_code(format!(\"{}\", tokens))"
impl cynic::schema::QueryRoot for Query {}
pub struct AnInputType;
impl cynic::schema::InputObjectMarker for AnInputType {}
pub struct DateTime {}
impl cynic::schema::NamedType for DateTime {
const NAME: &'static str = "DateTime";
}
pub struct Dessert {}
pub struct JSON {}
impl cynic::schema::NamedType for JSON {
Expand Down Expand Up @@ -181,6 +185,14 @@ pub mod __fields {
impl cynic::schema::HasField<json> for super::super::TestStruct {
type Type = Option<super::super::JSON>;
}
pub struct date;
impl cynic::schema::Field for date {
type Type = Option<super::super::DateTime>;
const NAME: &'static str = "date";
}
impl cynic::schema::HasField<date> for super::super::TestStruct {
type Type = Option<super::super::DateTime>;
}
pub struct __typename;
impl cynic::schema::Field for __typename {
type Type = super::super::String;
Expand Down
55 changes: 55 additions & 0 deletions cynic/tests/generics_simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,58 @@ fn test_generic_in_response() {
"###);
}

#[derive(cynic::QueryFragment, PartialEq, Debug)]
#[cynic(
schema_path = "../schemas/simple.graphql",
graphql_type = "Query",
variables = "TestArgs"
)]
struct GenericInResponseWithoutBounds<T> {
test_struct: Option<T>,
}

#[test]
fn test_generic_in_response_without_bounds() {
use cynic::QueryBuilder;

let operation =
GenericInResponseWithoutBounds::<TestStruct>::build(TestArgs { a_str: Some("1") });

insta::assert_snapshot!(operation.query, @r###"
query GenericInResponseWithoutBounds($aStr: String) {
testStruct {
fieldOne(x: 1, y: $aStr)
}
}
"###);
}

#[derive(cynic::QueryFragment, PartialEq, Debug)]
#[cynic(schema_path = "../schemas/simple.graphql", graphql_type = "TestStruct")]
struct TestStructWithoutArgs {
field_one: String,
}

#[derive(cynic::QueryFragment, PartialEq, Debug)]
#[cynic(schema_path = "../schemas/simple.graphql", graphql_type = "Query")]
struct GenericInResponseWithoutBoundsOrArgs<T> {
test_struct: Option<T>,
}

#[test]
fn test_generic_in_response_without_bounds_or_args() {
use cynic::QueryBuilder;

let operation = GenericInResponseWithoutBoundsOrArgs::<TestStructWithoutArgs>::build(());

insta::assert_snapshot!(operation.query, @r###"
query GenericInResponseWithoutBoundsOrArgs {
testStruct {
fieldOne
}
}
"###);
}
2 changes: 2 additions & 0 deletions schemas/simple.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
scalar JSON
scalar DateTime

type Query {
testStruct: TestStruct
Expand All @@ -13,6 +14,7 @@ type TestStruct {
optNested: Nested
dessert: Dessert
json: JSON
date: DateTime
}

union MyUnionType = Nested | TestStruct
Expand Down

0 comments on commit 4713ea6

Please sign in to comment.