Skip to content

Commit 6cf1859

Browse files
authored
fix: support &str for String input fields (#1160)
Previously this would cause a compile error as we generated a type marker of `&Option<str>` rather than `Option<&str>` - which would fail as the `Option<T>` has a `T: Sized` bound.
1 parent e9488c2 commit 6cf1859

File tree

5 files changed

+124
-8
lines changed

5 files changed

+124
-8
lines changed

cynic-codegen/src/types/alignment.rs

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
//! that are optional in rust but required in graphql - this is allowed,
66
//! but we need to do a bit of work for rust to be ok with it.
77
8+
use std::borrow::Cow;
9+
810
use syn::parse_quote;
911

1012
use crate::schema::types::{InputType, OutputType, TypeRef};
@@ -71,11 +73,38 @@ fn align_input_type_impl<'a>(
7173
gql_field_has_default: bool,
7274
) -> RustType<'a> {
7375
match (&ty, &gql_ty) {
74-
(RustType::Ref { inner, .. }, _) => {
75-
// Transform the inner types
76-
let new_inner = align_input_type_impl(inner.as_ref(), gql_ty, gql_field_has_default);
77-
ty.clone().replace_inner(new_inner)
76+
(RustType::Ref { inner, syn, span }, _) => {
77+
// We need to be careful with lifetimes - if we're doing any additional wrapping we need to
78+
// make sure that the reference goes _inside_ the wrapper, as otherwise we might end up
79+
// putting an unsized type inside an Option. e.g. `&'a Option<str>` which won't compile...
80+
match (inner.as_ref(), gql_ty) {
81+
(RustType::SimpleType { .. }, TypeRef::List(_)) => {
82+
let syn = Cow::Owned(parse_quote! { ::std::vec::Vec<#syn> });
83+
let wrapped_rust_type = RustType::List {
84+
syn,
85+
inner: Box::new(ty.clone()),
86+
span: *span,
87+
};
88+
align_input_type_impl(&wrapped_rust_type, gql_ty, false)
89+
}
90+
(RustType::SimpleType { .. }, TypeRef::Nullable(_)) => {
91+
let syn = Cow::Owned(parse_quote! { ::core::option::Option<#syn> });
92+
let wrapped_rust_type = RustType::Optional {
93+
syn,
94+
inner: Box::new(ty.clone()),
95+
span: *span,
96+
};
97+
align_input_type_impl(&wrapped_rust_type, gql_ty, false)
98+
}
99+
_ => {
100+
// Transform the inner types, preserving the reference.
101+
let new_inner =
102+
align_input_type_impl(inner.as_ref(), gql_ty, gql_field_has_default);
103+
ty.clone().replace_inner(new_inner)
104+
}
105+
}
78106
}
107+
79108
(RustType::List { inner, .. }, TypeRef::List(inner_gql)) => {
80109
// Transform the inner types
81110
let new_inner = align_input_type_impl(inner.as_ref(), inner_gql, false);
@@ -352,13 +381,41 @@ mod tests {
352381
let input_quote = quote! { #input };
353382
let result_quote = quote! { #result };
354383

355-
assert_eq!(input, result, "Expected {input_quote} got {result_quote}");
384+
assert_eq!(input, result, "expected {input_quote} got {result_quote}");
385+
}
386+
387+
#[test]
388+
fn test_align_reference_types() {
389+
let input = parse2(quote! { &'a str }).unwrap();
390+
391+
let result = align_input_type(&input, &nullable(string()), true);
392+
393+
let expected = quote! { ::core::option::Option<&'a str> }.to_string();
394+
let result = quote! { #result }.to_string();
395+
396+
assert_eq!(expected, result, "expected {expected} got {result}");
397+
}
398+
399+
#[test]
400+
fn test_align_double_nested_reference_types() {
401+
let input = parse2(quote! { &'a str }).unwrap();
402+
403+
let result = align_input_type(&input, &list(nullable(string())), true);
404+
405+
let result = quote! { #result }.to_string();
406+
let expected = quote! { ::std::vec::Vec<::core::option::Option<&'a str> > }.to_string();
407+
408+
assert_eq!(expected, result, "expected {expected} got {result}");
356409
}
357410

358411
fn integer<'a, Kind>() -> TypeRef<'a, Kind> {
359412
TypeRef::Named("Int".into(), PhantomData)
360413
}
361414

415+
fn string<'a, Kind>() -> TypeRef<'a, Kind> {
416+
TypeRef::Named("String".into(), PhantomData)
417+
}
418+
362419
fn list<Kind>(inner: TypeRef<'_, Kind>) -> TypeRef<'_, Kind> {
363420
TypeRef::List(Box::new(inner))
364421
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
source: cynic-codegen/tests/use-schema.rs
33
expression: "format_code(format!(\"{}\", tokens))"
4+
snapshot_kind: text
45
---
56
impl cynic::schema::QueryRoot for Foo {}
67
impl cynic::schema::MutationRoot for MutationRoot {}
@@ -104,6 +105,15 @@ pub mod __fields {
104105
const NAME: &'static ::core::primitive::str = "aString";
105106
}
106107
impl cynic::schema::HasInputField<aString, super::super::String> for super::super::Baz {}
108+
pub struct anOptionalString;
109+
impl cynic::schema::Field for anOptionalString {
110+
type Type = Option<super::super::String>;
111+
const NAME: &'static ::core::primitive::str = "anOptionalString";
112+
}
113+
impl cynic::schema::HasInputField<anOptionalString, Option<super::super::String>>
114+
for super::super::Baz
115+
{
116+
}
107117
}
108118
pub mod FieldNameClashes {
109119
pub struct str;
@@ -426,4 +436,3 @@ pub mod variable {
426436
const TYPE: VariableType = VariableType::Named("ID");
427437
}
428438
}
429-

cynic-parser/tests/snapshots/actual_schemas__test_cases__snapshot.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
source: cynic-parser/tests/actual_schemas.rs
33
expression: parsed.to_sdl_pretty()
4+
snapshot_kind: text
45
---
56
schema {
67
query: Foo
@@ -70,5 +71,5 @@ type MutationRoot {
7071
input Baz {
7172
id: ID!
7273
aString: String!
74+
anOptionalString: String
7375
}
74-

cynic/tests/mutation_generics.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,51 @@ fn test_query_building_nested_generic_in_vec() {
129129

130130
insta::assert_snapshot!(operation.query);
131131
}
132+
133+
#[test]
134+
fn test_with_optional_string_type() {
135+
#[derive(cynic::QueryFragment, Debug)]
136+
#[cynic(
137+
graphql_type = "MutationRoot",
138+
variables = "SendManyBazVariables",
139+
schema_path = "../schemas/test_cases.graphql",
140+
schema_module = "test_cases"
141+
)]
142+
pub struct SendManyBaz {
143+
#[arguments(many_baz: $many_baz)]
144+
#[allow(dead_code)]
145+
pub send_many_baz: Option<i32>,
146+
}
147+
148+
#[derive(cynic::QueryVariables, Debug)]
149+
#[cynic(schema_module = "test_cases")]
150+
pub struct SendManyBazVariables<'a, Id: cynic::schema::IsScalar<cynic::Id>> {
151+
#[cynic(graphql_type = "Vec<test_cases::Baz>")]
152+
pub many_baz: Vec<Baz<'a, Id>>,
153+
}
154+
155+
#[derive(cynic::InputObject, Debug)]
156+
#[cynic(
157+
schema_path = "../schemas/test_cases.graphql",
158+
schema_module = "test_cases"
159+
)]
160+
pub struct Baz<'a, Id: cynic::schema::IsScalar<cynic::Id>> {
161+
pub id: Id,
162+
pub a_string: &'a str,
163+
pub an_optional_string: &'a str,
164+
}
165+
166+
let operation = SendManyBaz::build(SendManyBazVariables {
167+
many_baz: vec![Baz {
168+
id: cynic::Id::new("some-totally-correct-id"),
169+
a_string: "baz",
170+
an_optional_string: "baz",
171+
}],
172+
});
173+
174+
insta::assert_snapshot!(operation.query, @r"
175+
mutation SendManyBaz($manyBaz: [Baz!]!) {
176+
sendManyBaz(many_baz: $manyBaz)
177+
}
178+
");
179+
}

schemas/test_cases.graphql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,6 @@ type MutationRoot {
6868
}
6969
input Baz {
7070
id: ID!
71-
aString: String! # TODO fix the nullable string case here
71+
aString: String!
72+
anOptionalString: String
7273
}

0 commit comments

Comments
 (0)