Skip to content

Commit da20eaf

Browse files
committed
feat: infer bounds for generic nested query fragments
1 parent a73244d commit da20eaf

File tree

5 files changed

+166
-0
lines changed

5 files changed

+166
-0
lines changed

cynic-codegen/src/fragment_derive/fragment_impl.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use darling::usage::{GenericsExt, Purpose, UsesTypeParams};
2+
use syn::{parse_quote, Token, WhereClause};
3+
14
use {
25
proc_macro2::{Span, TokenStream},
36
quote::{quote, quote_spanned},
@@ -28,6 +31,7 @@ pub struct FragmentImpl<'schema, 'a> {
2831
variables_fields: syn::Type,
2932
graphql_type_name: String,
3033
schema_type_path: syn::Path,
34+
additional_where: Option<syn::WhereClause>,
3135
}
3236

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

90+
let additional_where = additional_where_clause(
91+
generics,
92+
fields,
93+
schema,
94+
&field_module_path,
95+
variables_fields,
96+
);
97+
8698
let selections = fields
8799
.iter()
88100
.map(|(field, schema_field)| {
@@ -111,6 +123,7 @@ impl<'schema, 'a: 'schema> FragmentImpl<'schema, 'a> {
111123
variables_fields,
112124
graphql_type_name: graphql_type_name.to_string(),
113125
schema_type_path,
126+
additional_where,
114127
})
115128
}
116129
}
@@ -167,6 +180,69 @@ fn process_field<'a>(
167180
}))
168181
}
169182

183+
fn additional_where_clause(
184+
generics: &syn::Generics,
185+
fields: &[(FragmentDeriveField, Option<Field<'_>>)],
186+
schema: &Schema<'_, Unvalidated>,
187+
field_module_path: &syn::Path,
188+
variables_fields: Option<&syn::Path>,
189+
) -> Option<WhereClause> {
190+
let all_params = generics.declared_type_params();
191+
if all_params.is_empty() {
192+
return None;
193+
}
194+
let options = Purpose::BoundImpl.into();
195+
196+
let mut predicates: Vec<syn::WherePredicate> = vec![];
197+
198+
for (field, schema_field) in fields {
199+
let Some(schema_field) = schema_field else {
200+
continue;
201+
};
202+
let inner_type = schema_field.field_type.inner_type(schema);
203+
if !inner_type.is_composite() {
204+
// We only care about generics on composite types for now
205+
continue;
206+
}
207+
if *field.spread || *field.flatten {
208+
// We could probably support these, but I don't want to figure it out right now.
209+
// Leave it to the user to provide these bounds
210+
continue;
211+
}
212+
if field.ty.uses_type_params(&options, &all_params).is_empty() {
213+
// If this field uses no type params we skip it
214+
continue;
215+
}
216+
217+
let ty = &field.ty;
218+
let marker_ty = schema_field.marker_ident().to_path(field_module_path);
219+
predicates.push(parse_quote! {
220+
#ty: cynic::QueryFragment<SchemaType = <#marker_ty as cynic::schema::Field>::Type>
221+
});
222+
match variables_fields {
223+
Some(variables_fields) => {
224+
predicates.push(parse_quote! {
225+
#variables_fields: cynic::queries::VariableMatch<<#ty as cynic::QueryFragment>::VariablesFields>
226+
});
227+
}
228+
None => {
229+
predicates.push(parse_quote! {
230+
(): cynic::queries::VariableMatch<<#ty as cynic::QueryFragment>::VariablesFields>
231+
});
232+
}
233+
}
234+
}
235+
236+
if predicates.is_empty() {
237+
return None;
238+
}
239+
240+
Some(WhereClause {
241+
where_token: <Token![where]>::default(),
242+
predicates: predicates.into_iter().collect(),
243+
})
244+
}
245+
170246
impl quote::ToTokens for FragmentImpl<'_, '_> {
171247
fn to_tokens(&self, tokens: &mut TokenStream) {
172248
use quote::TokenStreamExt;
@@ -179,6 +255,17 @@ impl quote::ToTokens for FragmentImpl<'_, '_> {
179255
let fragment_name = proc_macro2::Literal::string(&target_struct.to_string());
180256
let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl();
181257

258+
let where_clause = match (where_clause, &self.additional_where) {
259+
(None, None) => None,
260+
(Some(lhs), None) => Some(quote! { #lhs }),
261+
(None, Some(rhs)) => Some(quote! { #rhs }),
262+
(Some(lhs), Some(rhs)) => {
263+
let mut new = lhs.clone();
264+
new.predicates.extend(rhs.predicates.clone());
265+
Some(quote! { #new })
266+
}
267+
};
268+
182269
tokens.append_all(quote! {
183270
#[automatically_derived]
184271
impl #impl_generics cynic::QueryFragment for #target_struct #ty_generics #where_clause {
@@ -383,6 +470,13 @@ impl quote::ToTokens for SpreadSelection {
383470
}
384471

385472
impl OutputType<'_> {
473+
fn is_composite(&self) -> bool {
474+
matches!(
475+
self,
476+
OutputType::Object(_) | OutputType::Interface(_) | OutputType::Union(_)
477+
)
478+
}
479+
386480
fn as_kind(&self) -> FieldKind {
387481
match self {
388482
OutputType::Scalar(_) => FieldKind::Scalar,

cynic-codegen/tests/snapshots/use_schema__simple.graphql.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ expression: "format_code(format!(\"{}\", tokens))"
55
impl cynic::schema::QueryRoot for Query {}
66
pub struct AnInputType;
77
impl cynic::schema::InputObjectMarker for AnInputType {}
8+
pub struct DateTime {}
9+
impl cynic::schema::NamedType for DateTime {
10+
const NAME: &'static ::core::primitive::str = "DateTime";
11+
}
812
pub struct Dessert {}
913
pub struct JSON {}
1014
impl cynic::schema::NamedType for JSON {
@@ -181,6 +185,14 @@ pub mod __fields {
181185
impl cynic::schema::HasField<json> for super::super::TestStruct {
182186
type Type = Option<super::super::JSON>;
183187
}
188+
pub struct date;
189+
impl cynic::schema::Field for date {
190+
type Type = Option<super::super::DateTime>;
191+
const NAME: &'static ::core::primitive::str = "date";
192+
}
193+
impl cynic::schema::HasField<date> for super::super::TestStruct {
194+
type Type = Option<super::super::DateTime>;
195+
}
184196
pub struct __typename;
185197
impl cynic::schema::Field for __typename {
186198
type Type = super::super::String;

cynic-parser/tests/snapshots/actual_schemas__simple__snapshot.snap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ expression: parsed.to_sdl()
44
---
55
scalar JSON
66

7+
scalar DateTime
8+
79
type Query {
810
testStruct: TestStruct
911
myUnion: MyUnionType
@@ -17,6 +19,7 @@ type TestStruct {
1719
optNested: Nested
1820
dessert: Dessert
1921
json: JSON
22+
date: DateTime
2023
}
2124

2225
union MyUnionType = Nested | TestStruct

cynic/tests/generics_simple.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,58 @@ fn test_generic_in_response() {
6262
6363
"###);
6464
}
65+
66+
#[derive(cynic::QueryFragment, PartialEq, Debug)]
67+
#[cynic(
68+
schema_path = "../schemas/simple.graphql",
69+
graphql_type = "Query",
70+
variables = "TestArgs"
71+
)]
72+
struct GenericInResponseWithoutBounds<T> {
73+
test_struct: Option<T>,
74+
}
75+
76+
#[test]
77+
fn test_generic_in_response_without_bounds() {
78+
use cynic::QueryBuilder;
79+
80+
let operation =
81+
GenericInResponseWithoutBounds::<TestStruct>::build(TestArgs { a_str: Some("1") });
82+
83+
insta::assert_snapshot!(operation.query, @r###"
84+
query GenericInResponseWithoutBounds($aStr: String) {
85+
testStruct {
86+
fieldOne(x: 1, y: $aStr)
87+
}
88+
}
89+
90+
"###);
91+
}
92+
93+
#[derive(cynic::QueryFragment, PartialEq, Debug)]
94+
#[cynic(schema_path = "../schemas/simple.graphql", graphql_type = "TestStruct")]
95+
struct TestStructWithoutArgs {
96+
field_one: String,
97+
}
98+
99+
#[derive(cynic::QueryFragment, PartialEq, Debug)]
100+
#[cynic(schema_path = "../schemas/simple.graphql", graphql_type = "Query")]
101+
struct GenericInResponseWithoutBoundsOrArgs<T> {
102+
test_struct: Option<T>,
103+
}
104+
105+
#[test]
106+
fn test_generic_in_response_without_bounds_or_args() {
107+
use cynic::QueryBuilder;
108+
109+
let operation = GenericInResponseWithoutBoundsOrArgs::<TestStructWithoutArgs>::build(());
110+
111+
insta::assert_snapshot!(operation.query, @r###"
112+
query GenericInResponseWithoutBoundsOrArgs {
113+
testStruct {
114+
fieldOne
115+
}
116+
}
117+
118+
"###);
119+
}

schemas/simple.graphql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
scalar JSON
2+
scalar DateTime
23

34
type Query {
45
testStruct: TestStruct
@@ -13,6 +14,7 @@ type TestStruct {
1314
optNested: Nested
1415
dessert: Dessert
1516
json: JSON
17+
date: DateTime
1618
}
1719

1820
union MyUnionType = Nested | TestStruct

0 commit comments

Comments
 (0)