Skip to content
This repository was archived by the owner on Mar 29, 2026. It is now read-only.

Commit 4c20a96

Browse files
feat: implement tristate optionals (some, null, undefined)
Fixes #125
1 parent 7439458 commit 4c20a96

File tree

19 files changed

+258
-11
lines changed

19 files changed

+258
-11
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cynic-codegen/src/types/parsing.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ pub fn parse_rust_type(ty: &syn::Type) -> RustType<'_> {
7878
};
7979
}
8080
}
81-
"Option" => {
81+
"MaybeUndefined" | "Option" => {
8282
if let Some(inner_type) = extract_generic_argument(last_segment) {
8383
return RustType::Optional {
8484
syn: Cow::Borrowed(type_path),

cynic-codegen/src/types/validation.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,17 @@ impl From<TypeValidationError> for syn::Error {
230230
let span = err.span();
231231
let message = match err {
232232
TypeValidationError::FieldIsOptional { provided_type, .. } =>
233-
format!("This field is nullable but you're not wrapping the type in Option. Did you mean Option<{}>", provided_type),
233+
format!("This field is nullable but you're not wrapping the type in MaybeUndefined or Option. Did you mean cynic::MaybeUndefined<{}> or Option<{}>", provided_type, provided_type),
234234
TypeValidationError::FieldIsRequired { provided_type, .. } =>
235-
format!("This field is not nullable but you're wrapping the type in Option. Did you mean {}", provided_type),
235+
format!("This field is not nullable but you're wrapping the type in Option or MaybeUndefined. Did you mean {}", provided_type),
236236
TypeValidationError::FieldIsList { provided_type, .. } => {
237237
format!("This field is a list but you're not wrapping the type in Vec. Did you mean Vec<{}>", provided_type)
238238
},
239239
TypeValidationError::FieldIsNotList { provided_type, .. } => {
240240
format!("This field is not a list but you're wrapping the type in Vec. Did you mean {}", provided_type)
241241
},
242242
TypeValidationError::RecursiveFieldWithoutOption { provided_type, .. } => {
243-
format!("Recursive types must be wrapped in Option. Did you mean Option<{}>", provided_type)
243+
format!("Recursive types must be wrapped in MaybeUndefined or Option. Did you mean cynic::MaybeUndefined<{}> or Option<{}>", provided_type, provided_type)
244244
}
245245
TypeValidationError::SpreadOnOption { .. } => "You can't spread on an optional field".to_string(),
246246
TypeValidationError::SpreadOnVec { .. } => "You can't spread on a list field".to_string(),

cynic-codegen/src/use_schema/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,13 @@ pub(crate) fn use_schema_impl(schema: &Schema<'_, Validated>) -> Result<TokenStr
151151
const TYPE: VariableType = VariableType::Nullable(&T::TYPE);
152152
}
153153

154+
impl<T> Variable for cynic::MaybeUndefined<T>
155+
where
156+
T: Variable
157+
{
158+
const TYPE: VariableType = VariableType::Nullable(&T::TYPE);
159+
}
160+
154161
impl<T> Variable for [T]
155162
where
156163
T: Variable,

cynic-querygen/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ once_cell = "1.9"
1818
rust_decimal = "1.22"
1919
thiserror = "1.0.30"
2020
uuid = { version = "1", features = ["v4"] }
21+
async-graphql = { version = "7" }
2122

2223
cynic-parser.workspace = true
2324

cynic-querygen/src/output/field.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ impl std::fmt::Display for Field<'_> {
4444
if let Some(rename) = self.rename() {
4545
writeln!(f, r#"#[cynic(rename = "{}")]"#, rename)?;
4646
}
47+
if self.type_spec.name.starts_with("cynic::MaybeUndefined<") {
48+
writeln!(
49+
f,
50+
r#"#[cynic(skip_serializing_if = "cynic::MaybeUndefined::is_undefined")]"#
51+
)?;
52+
}
4753
writeln!(f, "pub {}: {},", self.name(), self.type_spec.name)
4854
}
4955
}

cynic-querygen/src/schema/fields.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ fn input_type_spec_imp(
193193
needs_owned,
194194
is_subobject_with_lifetime,
195195
)
196-
.map(|type_spec| format!("Option<{type_spec}>",));
196+
.map(|type_spec| format!("cynic::MaybeUndefined<{type_spec}>",));
197197
}
198198

199199
match ty {

cynic/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ serde = { version = "1.0.136", features = ["derive"] }
3131
serde_json = { version = "1.0", optional = true }
3232
static_assertions = "1"
3333
thiserror = "1.0.30"
34+
async-graphql = { version = "7", default-features = false }
3435

3536
# Surf feature deps
3637
surf = { version = "2.3", default-features = false, optional = true }

cynic/src/coercions.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! certain changes to be made in a backwards compatible way, this module provides
55
//! some traits and macros to help enforce those.
66
7-
use crate::Id;
7+
use crate::{Id, MaybeUndefined};
88

99
/// Determines whether a type can be coerced into a given schema type.
1010
///
@@ -13,6 +13,12 @@ use crate::Id;
1313
pub trait CoercesTo<T> {}
1414

1515
impl<T, TypeLock> CoercesTo<Option<TypeLock>> for Option<T> where T: CoercesTo<TypeLock> {}
16+
impl<T, TypeLock> CoercesTo<Option<TypeLock>> for MaybeUndefined<T> where T: CoercesTo<TypeLock> {}
17+
impl<T, TypeLock> CoercesTo<MaybeUndefined<TypeLock>> for Option<T> where T: CoercesTo<TypeLock> {}
18+
impl<T, TypeLock> CoercesTo<MaybeUndefined<TypeLock>> for MaybeUndefined<T> where
19+
T: CoercesTo<TypeLock>
20+
{
21+
}
1622
impl<T, TypeLock> CoercesTo<Vec<TypeLock>> for Vec<T> where T: CoercesTo<TypeLock> {}
1723
impl<T, TypeLock> CoercesTo<Vec<TypeLock>> for [T] where T: CoercesTo<TypeLock> {}
1824

@@ -27,10 +33,14 @@ macro_rules! impl_coercions {
2733
($target:ty [$($impl_generics: tt)*] [$($where_clause: tt)*], $typelock:ty) => {
2834
impl $($impl_generics)* $crate::coercions::CoercesTo<$typelock> for $target $($where_clause)* {}
2935
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<$typelock>> for $target $($where_clause)* {}
36+
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<$typelock>> for $target $($where_clause)* {}
3037
impl $($impl_generics)* $crate::coercions::CoercesTo<Vec<$typelock>> for $target $($where_clause)* {}
3138
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<Vec<$typelock>>> for $target $($where_clause)* {}
3239
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<Vec<Option<$typelock>>>> for $target $($where_clause)* {}
3340
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<Option<$typelock>>> for $target $($where_clause)* {}
41+
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<Vec<$typelock>>> for $target $($where_clause)* {}
42+
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<Vec<$crate::MaybeUndefined<$typelock>>>> for $target $($where_clause)* {}
43+
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<$crate::MaybeUndefined<$typelock>>> for $target $($where_clause)* {}
3444
impl $($impl_generics)* $crate::coercions::CoercesTo<Vec<Vec<$typelock>>> for $target $($where_clause)* {}
3545
};
3646
}
@@ -63,17 +73,26 @@ mod tests {
6373
fn test_coercions() {
6474
assert_impl_all!(i32: CoercesTo<i32>);
6575
assert_impl_all!(i32: CoercesTo<Option<i32>>);
76+
assert_impl_all!(i32: CoercesTo<MaybeUndefined<i32>>);
6677
assert_impl_all!(i32: CoercesTo<Vec<i32>>);
6778
assert_impl_all!(i32: CoercesTo<Option<Vec<i32>>>);
79+
assert_impl_all!(i32: CoercesTo<MaybeUndefined<Vec<i32>>>);
6880
assert_impl_all!(i32: CoercesTo<Vec<Vec<i32>>>);
6981

7082
assert_impl_all!(Option<i32>: CoercesTo<Option<i32>>);
7183
assert_impl_all!(Option<i32>: CoercesTo<Option<Option<i32>>>);
7284

85+
assert_impl_all!(MaybeUndefined<i32>: CoercesTo<MaybeUndefined<i32>>);
86+
assert_impl_all!(MaybeUndefined<i32>: CoercesTo<MaybeUndefined<Option<i32>>>);
87+
88+
assert_impl_all!(MaybeUndefined<i32>: CoercesTo<Option<i32>>);
89+
assert_impl_all!(Option<i32>: CoercesTo<MaybeUndefined<i32>>);
90+
7391
assert_impl_all!(Vec<i32>: CoercesTo<Vec<i32>>);
7492
assert_impl_all!(Vec<i32>: CoercesTo<Vec<Vec<i32>>>);
7593

7694
assert_not_impl_any!(Vec<i32>: CoercesTo<i32>);
7795
assert_not_impl_any!(Option<i32>: CoercesTo<i32>);
96+
assert_not_impl_any!(MaybeUndefined<i32>: CoercesTo<i32>);
7897
}
7998
}

cynic/src/core.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::borrow::Cow;
22

3-
use crate::{queries::SelectionBuilder, QueryVariablesFields};
3+
use crate::{queries::SelectionBuilder, MaybeUndefined, QueryVariablesFields};
44

55
/// A trait that marks a type as part of a GraphQL query.
66
///
@@ -37,6 +37,18 @@ where
3737
}
3838
}
3939

40+
impl<T> QueryFragment for MaybeUndefined<T>
41+
where
42+
T: QueryFragment,
43+
{
44+
type SchemaType = Option<T::SchemaType>;
45+
type VariablesFields = T::VariablesFields;
46+
47+
fn query(builder: SelectionBuilder<'_, Self::SchemaType, Self::VariablesFields>) {
48+
T::query(builder.into_inner())
49+
}
50+
}
51+
4052
impl<T> QueryFragment for Vec<T>
4153
where
4254
T: QueryFragment,
@@ -163,6 +175,13 @@ where
163175
type SchemaType = Option<T::SchemaType>;
164176
}
165177

178+
impl<T> InputObject for MaybeUndefined<T>
179+
where
180+
T: InputObject,
181+
{
182+
type SchemaType = Option<T::SchemaType>;
183+
}
184+
166185
impl<T> InputObject for Vec<T>
167186
where
168187
T: InputObject,

0 commit comments

Comments
 (0)