Skip to content

Commit fe110fb

Browse files
authored
feat: support oneOf input objects in the generator (#1175)
1 parent c61ff61 commit fe110fb

File tree

13 files changed

+229
-23
lines changed

13 files changed

+229
-23
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
1212

1313
- The `QueryFragment` derive now supports the `rename_all` attribute
1414

15+
### Bug Fixes
16+
17+
- Fixed an issue with lifetime detection on nested input objects in the
18+
generator
19+
1520
## v3.12.0 - 2025-08-19
1621

1722
### New Features

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub struct FieldNameClashes;
1212
pub struct FlattenableEnums;
1313
pub struct Foo;
1414
pub struct MutationRoot;
15+
pub struct OneOfObject;
16+
impl cynic::schema::InputObjectMarker for OneOfObject {}
1517
pub struct RecursiveInputChild;
1618
impl cynic::schema::InputObjectMarker for RecursiveInputChild {}
1719
pub struct RecursiveInputParent;
@@ -281,6 +283,21 @@ pub mod __fields {
281283
const NAME: &'static ::core::primitive::str = "input";
282284
}
283285
}
286+
pub struct fieldWithOneOf;
287+
impl cynic::schema::Field for fieldWithOneOf {
288+
type Type = Option<super::super::Int>;
289+
const NAME: &'static ::core::primitive::str = "fieldWithOneOf";
290+
}
291+
impl cynic::schema::HasField<fieldWithOneOf> for super::super::Foo {
292+
type Type = Option<super::super::Int>;
293+
}
294+
pub mod _field_with_one_of_arguments {
295+
pub struct input;
296+
impl cynic::schema::HasArgument<input> for super::fieldWithOneOf {
297+
type ArgumentType = super::super::super::OneOfObject;
298+
const NAME: &'static ::core::primitive::str = "input";
299+
}
300+
}
284301
pub struct clashes;
285302
impl cynic::schema::Field for clashes {
286303
type Type = Option<super::super::FieldNameClashes>;
@@ -323,6 +340,29 @@ pub mod __fields {
323340
type Type = super::super::String;
324341
}
325342
}
343+
pub mod OneOfObject {
344+
pub struct string;
345+
impl cynic::schema::Field for string {
346+
type Type = Option<super::super::String>;
347+
const NAME: &'static ::core::primitive::str = "string";
348+
}
349+
impl cynic::schema::HasInputField<string, Option<super::super::String>>
350+
for super::super::OneOfObject
351+
{
352+
}
353+
pub struct int;
354+
impl cynic::schema::Field for int {
355+
type Type = Option<super::super::Int>;
356+
const NAME: &'static ::core::primitive::str = "int";
357+
}
358+
impl cynic::schema::HasInputField<int, Option<super::super::Int>> for super::super::OneOfObject {}
359+
pub struct nested;
360+
impl cynic::schema::Field for nested {
361+
type Type = Option<super::super::Baz>;
362+
const NAME: &'static ::core::primitive::str = "nested";
363+
}
364+
impl cynic::schema::HasInputField<nested, Option<super::super::Baz>> for super::super::OneOfObject {}
365+
}
326366
pub mod RecursiveInputChild {
327367
pub struct recurse;
328368
impl cynic::schema::Field for recurse {

cynic-parser/tests/snapshots/actual_schemas__test_cases__snapshot.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Foo {
2121
recursive2: RecursiveInputParent
2222
): Boolean
2323
fieldWithStringArg(input: String!): Int
24+
fieldWithOneOf(input: OneOfObject!): Int
2425
clashes: FieldNameClashes
2526
}
2627

@@ -73,3 +74,9 @@ input Baz {
7374
aString: String!
7475
anOptionalString: String
7576
}
77+
78+
input OneOfObject @oneOf {
79+
string: String
80+
int: Int
81+
nested: Baz
82+
}
Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod variant;
2+
13
use std::fmt::Write;
24

35
use crate::{casings::CasingExt, output::attr_output::Attributes, schema};
@@ -9,6 +11,7 @@ pub struct InputObject<'schema> {
911
pub name: String,
1012
pub fields: Vec<InputObjectField<'schema>>,
1113
pub schema_name: Option<String>,
14+
pub is_oneof: bool,
1215
}
1316

1417
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -32,28 +35,54 @@ impl std::fmt::Display for InputObject<'_> {
3235
}
3336

3437
write!(f, "{attributes}")?;
35-
writeln!(
36-
f,
37-
"pub struct {}{} {{",
38-
self.name.to_pascal_case(),
39-
schema::TypeSpec::lifetime(self.fields.iter().map(|f| &f.type_spec))
40-
)?;
41-
42-
for field in self.fields.iter() {
43-
let mut f = indented(f, 4);
44-
45-
let name = field.schema_field.name.to_snake_case();
46-
let mut output = super::Field::new(&name, &field.type_spec);
47-
48-
if name.to_camel_case() != field.schema_field.name {
49-
// If a snake -> camel casing roundtrip is not lossless
50-
// we need to explicitly rename this field
51-
output.add_rename(field.schema_field.name);
38+
if self.is_oneof {
39+
writeln!(
40+
f,
41+
"pub enum {}{} {{",
42+
self.name.to_pascal_case(),
43+
schema::TypeSpec::lifetime(self.fields.iter().map(|f| &f.type_spec))
44+
)?;
45+
46+
for field in self.fields.iter() {
47+
let mut f = indented(f, 4);
48+
49+
let name = field.schema_field.name.to_pascal_case();
50+
let mut output = variant::Variant::new(&name, &field.type_spec);
51+
52+
if name.to_snake_case() != field.schema_field.name {
53+
// If a snake -> pascal casing roundtrip is not lossless
54+
// we need to explicitly rename this field
55+
output.add_rename(field.schema_field.name);
56+
}
57+
58+
write!(f, "{}", output)?;
5259
}
5360

54-
write!(f, "{}", output)?;
55-
}
61+
writeln!(f, "}}")
62+
} else {
63+
writeln!(
64+
f,
65+
"pub struct {}{} {{",
66+
self.name.to_pascal_case(),
67+
schema::TypeSpec::lifetime(self.fields.iter().map(|f| &f.type_spec))
68+
)?;
5669

57-
writeln!(f, "}}")
70+
for field in self.fields.iter() {
71+
let mut f = indented(f, 4);
72+
73+
let name = field.schema_field.name.to_snake_case();
74+
let mut output = super::Field::new(&name, &field.type_spec);
75+
76+
if name.to_camel_case() != field.schema_field.name {
77+
// If a snake -> camel casing roundtrip is not lossless
78+
// we need to explicitly rename this field
79+
output.add_rename(field.schema_field.name);
80+
}
81+
82+
write!(f, "{}", output)?;
83+
}
84+
85+
writeln!(f, "}}")
86+
}
5887
}
5988
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use crate::schema::TypeSpec;
2+
3+
pub struct Variant<'a> {
4+
name: &'a str,
5+
rename: Option<&'a str>,
6+
type_spec: &'a TypeSpec<'a>,
7+
}
8+
9+
impl<'a> Variant<'a> {
10+
pub fn new(name: &'a str, type_spec: &'a TypeSpec<'a>) -> Self {
11+
Variant {
12+
name,
13+
type_spec,
14+
rename: None,
15+
}
16+
}
17+
18+
pub fn add_rename(&mut self, name: &'a str) {
19+
self.rename = Some(name);
20+
}
21+
22+
fn rename(&self) -> Option<&'a str> {
23+
if let Some(rename) = self.rename {
24+
return Some(rename);
25+
}
26+
27+
None
28+
}
29+
}
30+
31+
impl std::fmt::Display for Variant<'_> {
32+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33+
if let Some(rename) = self.rename() {
34+
writeln!(f, r#"#[cynic(rename = "{}")]"#, rename)?;
35+
}
36+
writeln!(f, "{}({}),", &self.name, self.type_spec.name)
37+
}
38+
}

cynic-querygen/src/query_parsing/inputs.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,18 @@ impl<'a> InputObjects<'a> {
6666
let requires_lifetime = self.objects_with_lifetime.contains(inner_type);
6767
crate::output::InputObjectField {
6868
schema_field: field.clone(),
69-
type_spec: field.value_type.type_spec(needs_boxed, requires_lifetime),
69+
type_spec: if object.is_oneof {
70+
field
71+
.value_type
72+
.oneof_type_spec(needs_boxed, requires_lifetime)
73+
} else {
74+
field.value_type.type_spec(needs_boxed, requires_lifetime)
75+
},
7076
}
7177
})
7278
.collect(),
7379
schema_name: None,
80+
is_oneof: object.is_oneof,
7481
})
7582
.collect()
7683
}
@@ -111,17 +118,21 @@ fn lifetimed_objects<'a>(document: &NormalisedDocument<'a, 'a>) -> HashSet<&'a s
111118
let mut stack = vec![variable.value_type.clone()];
112119
let mut visited = HashSet::new();
113120

114-
while !stack.is_empty() {
121+
'outer: while !stack.is_empty() {
115122
if let Ok(InputType::InputObject(object)) =
116123
stack.last().unwrap().inner_ref().lookup()
117124
{
125+
println!("checking fields of {}", object.name);
118126
for field in &object.fields {
119127
if !visited.contains(&field.value_type.inner_name()) {
120128
stack.push(field.value_type.clone());
121-
continue;
129+
visited.insert(field.value_type.inner_name().into());
130+
continue 'outer;
122131
}
123132
}
124133

134+
println!("visiting {}", object.name);
135+
125136
// If we get here all child field types have been seen.
126137
// We need to check whether any child fields need a lifetime...
127138
for field in object.fields {

cynic-querygen/src/schema/fields.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ impl<'schema> InputFieldType<'schema> {
125125
) -> TypeSpec<'static> {
126126
input_type_spec_imp(self, true, needs_boxed, is_subobject_with_lifetime)
127127
}
128+
129+
pub fn oneof_type_spec(
130+
&self,
131+
needs_boxed: bool,
132+
is_subobject_with_lifetime: bool,
133+
) -> TypeSpec<'static> {
134+
input_type_spec_imp(self, false, needs_boxed, is_subobject_with_lifetime)
135+
}
128136
}
129137

130138
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]

cynic-querygen/src/schema/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub struct UnionDetails<'schema> {
5959
pub struct InputObjectDetails<'schema> {
6060
pub name: &'schema str,
6161
pub fields: Vec<InputField<'schema>>,
62+
pub is_oneof: bool,
6263
}
6364

6465
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
@@ -129,6 +130,9 @@ impl<'schema> Type<'schema> {
129130
.fields()
130131
.map(|field| InputField::from_parser(field, type_index))
131132
.collect(),
133+
is_oneof: obj
134+
.directives()
135+
.any(|directive| directive.name() == "oneOf"),
132136
}),
133137
}
134138
}

cynic-querygen/tests/misc-tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,18 @@ fn test_recursive_input_lifetimes() {
156156
.expect("QueryGen Failed for recursive input lifetimes test")
157157
)
158158
}
159+
160+
#[test]
161+
fn test_oneof_inputs() {
162+
let schema = include_str!("../../schemas/test_cases.graphql");
163+
let query = r#"
164+
query MyQuery($input: OneOfObject!) {
165+
fieldWithOneOf(input: $input)
166+
}
167+
"#;
168+
169+
assert_snapshot!(
170+
document_to_fragment_structs(query, schema, &QueryGenOptions::default())
171+
.expect("QueryGen Failed")
172+
)
173+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
source: cynic-querygen/tests/misc-tests.rs
3+
expression: "document_to_fragment_structs(query, schema,\n&QueryGenOptions::default()).expect(\"QueryGen Failed\")"
4+
snapshot_kind: text
5+
---
6+
#[derive(cynic::QueryVariables, Debug)]
7+
pub struct MyQueryVariables<'a> {
8+
pub input: OneOfObject<'a>,
9+
}
10+
11+
#[derive(cynic::QueryFragment, Debug)]
12+
#[cynic(graphql_type = "Foo", variables = "MyQueryVariables")]
13+
pub struct MyQuery {
14+
#[arguments(input: $input)]
15+
pub field_with_one_of: Option<i32>,
16+
}
17+
18+
#[derive(cynic::InputObject, Debug)]
19+
pub enum OneOfObject<'a> {
20+
String(&'a str),
21+
Int(i32),
22+
Nested(Baz<'a>),
23+
}
24+
25+
#[derive(cynic::InputObject, Debug)]
26+
pub struct Baz<'a> {
27+
pub id: &'a cynic::Id,
28+
pub a_string: &'a str,
29+
pub an_optional_string: Option<&'a str>,
30+
}

0 commit comments

Comments
 (0)