Skip to content

Commit a5f274b

Browse files
committed
Support recursive fragments
1 parent c01d2f4 commit a5f274b

File tree

14 files changed

+287
-88
lines changed

14 files changed

+287
-88
lines changed

graphql_client/tests/fragments.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,25 @@ fn fragments_with_snake_case_name() {
5555
"value"
5656
);
5757
}
58+
59+
#[derive(GraphQLQuery)]
60+
#[graphql(
61+
query_path = "tests/fragments/query.graphql",
62+
schema_path = "tests/fragments/schema.graphql"
63+
)]
64+
pub struct RecursiveFragmentQuery;
65+
66+
#[test]
67+
fn recursive_fragment() {
68+
use recursive_fragment_query::*;
69+
70+
let _ = RecursiveFragment {
71+
head: Some("ABCD".to_string()),
72+
tail: Some(RecursiveFragmentTail {
73+
recursive_fragment: Box::new(RecursiveFragment {
74+
head: Some("EFGH".to_string()),
75+
tail: None,
76+
}),
77+
}),
78+
};
79+
}

graphql_client/tests/fragments/query.graphql

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,23 @@ fragment snake_case_fragment on QueryRoot {
66
inFragment
77
}
88

9+
fragment RecursiveFragment on RecursiveNode {
10+
head
11+
tail {
12+
...RecursiveFragment
13+
}
14+
}
15+
916
query FragmentReference {
1017
...FragmentReference
1118
}
1219

1320
query SnakeCaseFragment {
1421
...snake_case_fragment
1522
}
23+
24+
query RecursiveFragmentQuery {
25+
recursive {
26+
...RecursiveFragment
27+
}
28+
}

graphql_client/tests/fragments/schema.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ schema {
22
query: QueryRoot
33
}
44

5+
type RecursiveNode {
6+
head: String
7+
tail: RecursiveNode
8+
}
9+
510
type QueryRoot {
611
extra: String
712
inFragment: String
13+
recursive: RecursiveNode!
814
}

graphql_client/tests/input_object_variables.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,29 @@ fn recursive_input_objects_can_be_constructed() {
7474
})),
7575
};
7676
}
77+
78+
#[derive(GraphQLQuery)]
79+
#[graphql(
80+
query_path = "tests/input_object_variables/input_object_variables_query.graphql",
81+
schema_path = "tests/input_object_variables/input_object_variables_schema.graphql",
82+
response_derives = "Debug, PartialEq"
83+
)]
84+
pub struct IndirectlyRecursiveInputQuery;
85+
86+
#[test]
87+
fn indirectly_recursive_input_objects_can_be_constructed() {
88+
use indirectly_recursive_input_query::*;
89+
90+
IndirectlyRecursiveInput {
91+
head: "hello".to_string(),
92+
tail: Box::new(None),
93+
};
94+
95+
IndirectlyRecursiveInput {
96+
head: "hi".to_string(),
97+
tail: Box::new(Some(IndirectlyRecursiveInputTailPart {
98+
name: "this is crazy".to_string(),
99+
recursed_field: Box::new(None),
100+
})),
101+
};
102+
}

graphql_client/tests/input_object_variables/input_object_variables_query.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ query VariablesQuery($msg: Message) {
77
query RecursiveInputQuery($input: RecursiveInput!) {
88
saveRecursiveInput(recursiveInput: $input)
99
}
10+
11+
query IndirectlyRecursiveInputQuery($input: IndirectlyRecursiveInput!) {
12+
saveRecursiveInput(recursiveInput: $input)
13+
}

graphql_client/tests/input_object_variables/input_object_variables_schema.graphql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ input RecursiveInput {
2929
tail: RecursiveInput
3030
}
3131

32+
input IndirectlyRecursiveInput {
33+
head: String!
34+
tail: IndirectlyRecursiveInputTailPart
35+
}
36+
37+
input IndirectlyRecursiveInputTailPart {
38+
name: String!
39+
recursed_field: IndirectlyRecursiveInput
40+
}
41+
3242
type InputObjectVariablesQuery {
3343
echo(message: Message!, options: Options = { pgpSignature: true }): EchoResult
3444
saveRecursiveInput(recursiveInput: RecursiveInput!): Category

graphql_client_codegen/src/codegen.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ pub(crate) fn response_for_query(
5555
query::Definition::Operation(_op) => (),
5656
query::Definition::Fragment(fragment) => {
5757
let &query::TypeCondition::On(ref on) = &fragment.type_condition;
58+
let on = schema.fragment_target(on).ok_or_else(|| {
59+
format_err!(
60+
"Fragment {} is defined on unknown type: {}",
61+
&fragment.name,
62+
on,
63+
)
64+
})?;
5865
context.fragments.insert(
5966
&fragment.name,
6067
GqlFragment {
@@ -82,7 +89,7 @@ pub(crate) fn response_for_query(
8289
let prefix = &operation.name;
8390
let selection = &operation.selection;
8491

85-
if operation.is_subscription() && selection.0.len() > 1 {
92+
if operation.is_subscription() && selection.len() > 1 {
8693
Err(format_err!(
8794
"{}",
8895
::constants::MULTIPLE_SUBSCRIPTION_FIELDS_ERROR

graphql_client_codegen/src/fragments.rs

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,31 @@ use query::QueryContext;
33
use selection::Selection;
44
use std::cell::Cell;
55

6+
/// Represents which type a fragment is defined on. This is the type mentioned in the fragment's `on` clause.
7+
#[derive(Debug, PartialEq)]
8+
pub(crate) enum FragmentTarget<'context> {
9+
Object(&'context crate::objects::GqlObject<'context>),
10+
Interface(&'context crate::interfaces::GqlInterface<'context>),
11+
Union(&'context crate::unions::GqlUnion<'context>),
12+
}
13+
14+
impl<'context> FragmentTarget<'context> {
15+
pub(crate) fn name(&self) -> &str {
16+
match self {
17+
FragmentTarget::Object(obj) => obj.name,
18+
FragmentTarget::Interface(iface) => iface.name,
19+
FragmentTarget::Union(unn) => unn.name,
20+
}
21+
}
22+
}
23+
624
/// Represents a fragment extracted from a query document.
725
#[derive(Debug, PartialEq)]
826
pub(crate) struct GqlFragment<'query> {
927
/// The name of the fragment, matching one-to-one with the name in the GraphQL query document.
1028
pub name: &'query str,
1129
/// The `on` clause of the fragment.
12-
pub on: &'query str,
30+
pub on: FragmentTarget<'query>,
1331
/// The selected fields.
1432
pub selection: Selection<'query>,
1533
/// Whether the fragment is used in the current query
@@ -19,16 +37,20 @@ pub(crate) struct GqlFragment<'query> {
1937
impl<'query> GqlFragment<'query> {
2038
/// Generate all the Rust code required by the fragment's object selection.
2139
pub(crate) fn to_rust(&self, context: &QueryContext) -> Result<TokenStream, ::failure::Error> {
22-
if let Some(obj) = context.schema.objects.get(&self.on) {
23-
obj.response_for_selection(context, &self.selection, &self.name)
24-
} else if let Some(iface) = context.schema.interfaces.get(&self.on) {
25-
iface.response_for_selection(context, &self.selection, &self.name)
26-
} else {
27-
Err(format_err!(
28-
"Fragment {} is defined on unknown type: {}",
29-
self.name,
30-
self.on
31-
))?
40+
match self.on {
41+
FragmentTarget::Object(obj) => {
42+
obj.response_for_selection(context, &self.selection, &self.name)
43+
}
44+
FragmentTarget::Interface(iface) => {
45+
iface.response_for_selection(context, &self.selection, &self.name)
46+
}
47+
FragmentTarget::Union(_) => {
48+
unreachable!("Wrong code path. Fragment on unions are treated differently.")
49+
}
3250
}
3351
}
52+
53+
pub(crate) fn is_recursive(&self) -> bool {
54+
self.selection.contains_fragment(&self.name)
55+
}
3456
}

graphql_client_codegen/src/inputs.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,36 @@ impl<'schema> GqlInput<'schema> {
3030
})
3131
}
3232

33+
fn contains_type_without_indirection(&self, context: &QueryContext, type_name: &str) -> bool {
34+
// the input type is recursive if any of its members contains it, without indirection
35+
self.fields.values().any(|field| {
36+
// the field is indirected, so no boxing is needed
37+
if field.type_.is_indirected() {
38+
return false;
39+
}
40+
41+
let field_type_name = field.type_.inner_name_str();
42+
let input = context.schema.inputs.get(field_type_name);
43+
44+
if let Some(input) = input {
45+
// the input contains itself, not indirected
46+
if input.name == type_name {
47+
return true;
48+
}
49+
50+
// we check if the other input contains this one (without indirection)
51+
input.contains_type_without_indirection(context, type_name)
52+
} else {
53+
// the field is not referring to an input type
54+
false
55+
}
56+
})
57+
}
58+
59+
fn is_recursive_without_indirection(&self, context: &QueryContext) -> bool {
60+
self.contains_type_without_indirection(context, &self.name)
61+
}
62+
3363
pub(crate) fn to_rust(&self, context: &QueryContext) -> Result<TokenStream, failure::Error> {
3464
let name = Ident::new(&self.name, Span::call_site());
3565
let mut fields: Vec<&GqlObjectField> = self.fields.values().collect();
@@ -38,10 +68,14 @@ impl<'schema> GqlInput<'schema> {
3868
let ty = field.type_.to_rust(&context, "");
3969

4070
// If the type is recursive, we have to box it
41-
let ty = if field.type_.is_indirected() || field.type_.inner_name_str() != self.name {
42-
ty
71+
let ty = if let Some(input) = context.schema.inputs.get(field.type_.inner_name_str()) {
72+
if input.is_recursive_without_indirection(context) {
73+
quote! { Box<#ty> }
74+
} else {
75+
quote!(#ty)
76+
}
4377
} else {
44-
quote! { Box<#ty> }
78+
quote!(#ty)
4579
};
4680

4781
context.schema.require(&field.type_.inner_name_str());

graphql_client_codegen/src/interfaces.rs

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -34,59 +34,53 @@ impl<'schema> GqlInterface<'schema> {
3434
selection: &'query Selection<'query>,
3535
query_context: &QueryContext,
3636
) -> Selection<'query> {
37-
Selection(
38-
selection
39-
.0
40-
.iter()
41-
// Only keep what we can handle
42-
.filter(|f| match f {
43-
SelectionItem::Field(f) => f.name != TYPENAME_FIELD,
44-
SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => {
45-
// only if the fragment refers to the interface’s own fields (to take into account type-refining fragments)
46-
let fragment = query_context
47-
.fragments
48-
.get(fragment_name)
49-
.ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))
50-
// TODO: fix this
51-
.unwrap();
37+
(&selection)
38+
.into_iter()
39+
// Only keep what we can handle
40+
.filter(|f| match f {
41+
SelectionItem::Field(f) => f.name != TYPENAME_FIELD,
42+
SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => {
43+
// only if the fragment refers to the interface’s own fields (to take into account type-refining fragments)
44+
let fragment = query_context
45+
.fragments
46+
.get(fragment_name)
47+
.ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))
48+
// TODO: fix this
49+
.unwrap();
5250

53-
fragment.on == self.name
54-
}
55-
SelectionItem::InlineFragment(_) => false,
56-
})
57-
.map(|a| (*a).clone())
58-
.collect(),
59-
)
51+
fragment.on.name() == self.name
52+
}
53+
SelectionItem::InlineFragment(_) => false,
54+
})
55+
.map(|a| (*a).clone())
56+
.collect()
6057
}
6158

6259
fn union_selection<'query>(
6360
&self,
6461
selection: &'query Selection,
6562
query_context: &QueryContext,
6663
) -> Selection<'query> {
67-
Selection(
68-
selection
69-
.0
70-
.iter()
71-
// Only keep what we can handle
72-
.filter(|f| match f {
73-
SelectionItem::InlineFragment(_) => true,
74-
SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => {
75-
let fragment = query_context
76-
.fragments
77-
.get(fragment_name)
78-
.ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))
79-
// TODO: fix this
80-
.unwrap();
64+
(&selection)
65+
.into_iter()
66+
// Only keep what we can handle
67+
.filter(|f| match f {
68+
SelectionItem::InlineFragment(_) => true,
69+
SelectionItem::FragmentSpread(SelectionFragmentSpread { fragment_name }) => {
70+
let fragment = query_context
71+
.fragments
72+
.get(fragment_name)
73+
.ok_or_else(|| format_err!("Unknown fragment: {}", &fragment_name))
74+
// TODO: fix this
75+
.unwrap();
8176

82-
// only the fragments _not_ on the interface
83-
fragment.on != self.name
84-
}
85-
SelectionItem::Field(SelectionField { name, .. }) => *name == "__typename",
86-
})
87-
.map(|a| (*a).clone())
88-
.collect(),
89-
)
77+
// only the fragments _not_ on the interface
78+
fragment.on.name() != self.name
79+
}
80+
SelectionItem::Field(SelectionField { name, .. }) => *name == "__typename",
81+
})
82+
.map(|a| (*a).clone())
83+
.collect()
9084
}
9185

9286
/// Create an empty interface. This needs to be mutated before it is useful.
@@ -227,13 +221,13 @@ mod tests {
227221
let typename_field = ::selection::SelectionItem::Field(::selection::SelectionField {
228222
alias: None,
229223
name: "__typename",
230-
fields: Selection(vec![]),
224+
fields: Selection::new_empty(),
231225
});
232-
let selection = Selection(vec![typename_field.clone()]);
226+
let selection = Selection::from_vec(vec![typename_field.clone()]);
233227

234228
assert_eq!(
235229
iface.union_selection(&selection, &context),
236-
Selection(vec![typename_field])
230+
Selection::from_vec(vec![typename_field])
237231
);
238232
}
239233

@@ -254,13 +248,13 @@ mod tests {
254248
let typename_field = ::selection::SelectionItem::Field(::selection::SelectionField {
255249
alias: None,
256250
name: "__typename",
257-
fields: Selection(vec![]),
251+
fields: Selection::new_empty(),
258252
});
259-
let selection = Selection(vec![typename_field]);
253+
let selection: Selection = vec![typename_field].into_iter().collect();
260254

261255
assert_eq!(
262256
iface.object_selection(&selection, &context),
263-
Selection(vec![])
257+
Selection::new_empty()
264258
);
265259
}
266260
}

0 commit comments

Comments
 (0)