Skip to content

Commit 004d745

Browse files
authored
Merge pull request #211 from graphql-rust/recursive-fragments
Support recursive fragments
2 parents c01d2f4 + 536ec2e commit 004d745

File tree

15 files changed

+301
-88
lines changed

15 files changed

+301
-88
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## Unreleased
99

10+
### Added
11+
12+
- If there is no `schema` declaration in a schema, the root types will be matched by name (`Query`, `Mutation` and `Subscription`).
13+
14+
### Changed
15+
16+
- Enums now always derive PartialEq and Eq by default
17+
18+
### Fixed
19+
20+
- Code generation for fragments on unions was fixed
21+
- Support for recursive fragments and input types
22+
- The graphql-parser dependency version is no longer pinned
23+
1024
## 0.6.0 (2018-12-30)
1125

1226
### Added

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());

0 commit comments

Comments
 (0)