Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cynic-codegen/src/types/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub fn parse_rust_type(ty: &syn::Type) -> RustType<'_> {
};
}
}
"Option" => {
"MaybeUndefined" | "Option" => {
if let Some(inner_type) = extract_generic_argument(last_segment) {
return RustType::Optional {
syn: Cow::Borrowed(type_path),
Expand Down
6 changes: 3 additions & 3 deletions cynic-codegen/src/types/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,17 @@ impl From<TypeValidationError> for syn::Error {
let span = err.span();
let message = match err {
TypeValidationError::FieldIsOptional { provided_type, .. } =>
format!("This field is nullable but you're not wrapping the type in Option. Did you mean Option<{}>", provided_type),
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),
TypeValidationError::FieldIsRequired { provided_type, .. } =>
format!("This field is not nullable but you're wrapping the type in Option. Did you mean {}", provided_type),
format!("This field is not nullable but you're wrapping the type in Option or MaybeUndefined. Did you mean {}", provided_type),
TypeValidationError::FieldIsList { provided_type, .. } => {
format!("This field is a list but you're not wrapping the type in Vec. Did you mean Vec<{}>", provided_type)
},
TypeValidationError::FieldIsNotList { provided_type, .. } => {
format!("This field is not a list but you're wrapping the type in Vec. Did you mean {}", provided_type)
},
TypeValidationError::RecursiveFieldWithoutOption { provided_type, .. } => {
format!("Recursive types must be wrapped in Option. Did you mean Option<{}>", provided_type)
format!("Recursive types must be wrapped in MaybeUndefined or Option. Did you mean cynic::MaybeUndefined<{}> or Option<{}>", provided_type, provided_type)
}
TypeValidationError::SpreadOnOption { .. } => "You can't spread on an optional field".to_string(),
TypeValidationError::SpreadOnVec { .. } => "You can't spread on a list field".to_string(),
Expand Down
7 changes: 7 additions & 0 deletions cynic-codegen/src/use_schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ pub(crate) fn use_schema_impl(schema: &Schema<'_, Validated>) -> Result<TokenStr
const TYPE: VariableType = VariableType::Nullable(&T::TYPE);
}

impl<T> Variable for cynic::MaybeUndefined<T>
where
T: Variable
{
const TYPE: VariableType = VariableType::Nullable(&T::TYPE);
}

impl<T> Variable for [T]
where
T: Variable,
Expand Down
1 change: 1 addition & 0 deletions cynic-querygen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ once_cell = "1.9"
rust_decimal = "1.22"
thiserror = "1.0.30"
uuid = { version = "1", features = ["v4"] }
async-graphql = { version = "7" }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async-graphql is a very heavy dependency to pull in, particularly just for a single 3 variant enum. Can you implement this without the dependency please.


cynic-parser.workspace = true

Expand Down
6 changes: 6 additions & 0 deletions cynic-querygen/src/output/field.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ impl std::fmt::Display for Field<'_> {
if let Some(rename) = self.rename() {
writeln!(f, r#"#[cynic(rename = "{}")]"#, rename)?;
}
if self.type_spec.name.starts_with("cynic::MaybeUndefined<") {
writeln!(
f,
r#"#[cynic(skip_serializing_if = "cynic::MaybeUndefined::is_undefined")]"#
)?;
}
writeln!(f, "pub {}: {},", self.name(), self.type_spec.name)
}
}
Expand Down
2 changes: 1 addition & 1 deletion cynic-querygen/src/schema/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ fn input_type_spec_imp(
needs_owned,
is_subobject_with_lifetime,
)
.map(|type_spec| format!("Option<{type_spec}>",));
.map(|type_spec| format!("cynic::MaybeUndefined<{type_spec}>",));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to force every user of the generator into using MaybeUndefined. I'd rather this was controlled by a parameter somewhere - either a parameter passed in rust or maybe a directive in the query that we're generating from?

}

match ty {
Expand Down
1 change: 1 addition & 0 deletions cynic/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ serde = { version = "1.0.136", features = ["derive"] }
serde_json = { version = "1.0", optional = true }
static_assertions = "1"
thiserror = "1.0.30"
async-graphql = { version = "7", default-features = false }

# Surf feature deps
surf = { version = "2.3", default-features = false, optional = true }
Expand Down
21 changes: 20 additions & 1 deletion cynic/src/coercions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! certain changes to be made in a backwards compatible way, this module provides
//! some traits and macros to help enforce those.

use crate::Id;
use crate::{Id, MaybeUndefined};

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

impl<T, TypeLock> CoercesTo<Option<TypeLock>> for Option<T> where T: CoercesTo<TypeLock> {}
impl<T, TypeLock> CoercesTo<Option<TypeLock>> for MaybeUndefined<T> where T: CoercesTo<TypeLock> {}
impl<T, TypeLock> CoercesTo<MaybeUndefined<TypeLock>> for Option<T> where T: CoercesTo<TypeLock> {}
impl<T, TypeLock> CoercesTo<MaybeUndefined<TypeLock>> for MaybeUndefined<T> where
T: CoercesTo<TypeLock>
{
}
Comment on lines +17 to +21
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure these CorecesTo<MaybeUndefined<... impls are needed? I feel like they might not be....

impl<T, TypeLock> CoercesTo<Vec<TypeLock>> for Vec<T> where T: CoercesTo<TypeLock> {}
impl<T, TypeLock> CoercesTo<Vec<TypeLock>> for [T] where T: CoercesTo<TypeLock> {}

Expand All @@ -27,10 +33,14 @@ macro_rules! impl_coercions {
($target:ty [$($impl_generics: tt)*] [$($where_clause: tt)*], $typelock:ty) => {
impl $($impl_generics)* $crate::coercions::CoercesTo<$typelock> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<$typelock>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<$typelock>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<Vec<$typelock>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<Vec<$typelock>>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<Vec<Option<$typelock>>>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<Option<Option<$typelock>>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<Vec<$typelock>>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<Vec<$crate::MaybeUndefined<$typelock>>>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<$crate::MaybeUndefined<$crate::MaybeUndefined<$typelock>>> for $target $($where_clause)* {}
impl $($impl_generics)* $crate::coercions::CoercesTo<Vec<Vec<$typelock>>> for $target $($where_clause)* {}
};
}
Expand Down Expand Up @@ -63,17 +73,26 @@ mod tests {
fn test_coercions() {
assert_impl_all!(i32: CoercesTo<i32>);
assert_impl_all!(i32: CoercesTo<Option<i32>>);
assert_impl_all!(i32: CoercesTo<MaybeUndefined<i32>>);
assert_impl_all!(i32: CoercesTo<Vec<i32>>);
assert_impl_all!(i32: CoercesTo<Option<Vec<i32>>>);
assert_impl_all!(i32: CoercesTo<MaybeUndefined<Vec<i32>>>);
assert_impl_all!(i32: CoercesTo<Vec<Vec<i32>>>);

assert_impl_all!(Option<i32>: CoercesTo<Option<i32>>);
assert_impl_all!(Option<i32>: CoercesTo<Option<Option<i32>>>);

assert_impl_all!(MaybeUndefined<i32>: CoercesTo<MaybeUndefined<i32>>);
assert_impl_all!(MaybeUndefined<i32>: CoercesTo<MaybeUndefined<Option<i32>>>);

assert_impl_all!(MaybeUndefined<i32>: CoercesTo<Option<i32>>);
assert_impl_all!(Option<i32>: CoercesTo<MaybeUndefined<i32>>);

assert_impl_all!(Vec<i32>: CoercesTo<Vec<i32>>);
assert_impl_all!(Vec<i32>: CoercesTo<Vec<Vec<i32>>>);

assert_not_impl_any!(Vec<i32>: CoercesTo<i32>);
assert_not_impl_any!(Option<i32>: CoercesTo<i32>);
assert_not_impl_any!(MaybeUndefined<i32>: CoercesTo<i32>);
}
}
21 changes: 20 additions & 1 deletion cynic/src/core.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::borrow::Cow;

use crate::{queries::SelectionBuilder, QueryVariablesFields};
use crate::{queries::SelectionBuilder, MaybeUndefined, QueryVariablesFields};

/// A trait that marks a type as part of a GraphQL query.
///
Expand Down Expand Up @@ -37,6 +37,18 @@ where
}
}

impl<T> QueryFragment for MaybeUndefined<T>
where
T: QueryFragment,
{
type SchemaType = Option<T::SchemaType>;
type VariablesFields = T::VariablesFields;

fn query(builder: SelectionBuilder<'_, Self::SchemaType, Self::VariablesFields>) {
T::query(builder.into_inner())
}
}
Comment on lines +40 to +50
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really sure is there's a use case for MaybeUndefined as a QueryFragment. It makes sense for input types but not so much for output types which will never be undefined in a well formed graphql response....


impl<T> QueryFragment for Vec<T>
where
T: QueryFragment,
Expand Down Expand Up @@ -163,6 +175,13 @@ where
type SchemaType = Option<T::SchemaType>;
}

impl<T> InputObject for MaybeUndefined<T>
where
T: InputObject,
{
type SchemaType = Option<T::SchemaType>;
}

impl<T> InputObject for Vec<T>
where
T: InputObject,
Expand Down
2 changes: 2 additions & 0 deletions cynic/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
mod builders;
mod core;
mod id;
mod maybe_undefined;
mod operation;
mod result;

Expand All @@ -189,6 +190,7 @@ pub use {
self::core::{Enum, InlineFragments, InputObject, QueryFragment},
builders::{MutationBuilder, QueryBuilder, SubscriptionBuilder},
id::Id,
maybe_undefined::MaybeUndefined,
operation::{Operation, OperationBuildError, OperationBuilder, StreamingOperation},
result::*,
variables::{QueryVariableLiterals, QueryVariables, QueryVariablesFields},
Expand Down
87 changes: 87 additions & 0 deletions cynic/src/maybe_undefined.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use serde::{Deserialize, Serialize};

use std::ops::{Deref, DerefMut};

/// A wrapper around async-graphql's [`MaybeUndefined`](https://docs.rs/async-graphql/latest/async_graphql/types/enum.MaybeUndefined.html).
///
/// You can initialize it from:
/// - `From<Option<T>>` will become T or null.
/// - `Default` will become undefined.
#[derive(Serialize, Deserialize, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
pub struct MaybeUndefined<T>(async_graphql::MaybeUndefined<T>);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I already mentioned can you implement MaybeUndefined here rather than pulling in async_graphql


impl<T> MaybeUndefined<T> {
fn inner(&self) -> &async_graphql::MaybeUndefined<T> {
&self.0
}

fn inner_mut(&mut self) -> &mut async_graphql::MaybeUndefined<T> {
&mut self.0
}

/// Returns true if the `MaybeUndefined<T>` is undefined.
///
/// Deserialization ought to be skipped for this when used as a field.
pub fn is_undefined(&self) -> bool {
self.0.is_undefined()
}
}

impl<T> From<async_graphql::MaybeUndefined<T>> for MaybeUndefined<T> {
fn from(value: async_graphql::MaybeUndefined<T>) -> Self {
Self(value)
}
}

impl<T> Deref for MaybeUndefined<T> {
type Target = async_graphql::MaybeUndefined<T>;

fn deref(&self) -> &Self::Target {
self.inner()
}
}

impl<T> DerefMut for MaybeUndefined<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner_mut()
}
}

impl<T1, T2> From<Option<T1>> for MaybeUndefined<T2>
where
T2: From<T1>,
{
fn from(value: Option<T1>) -> Self {
Self(match value {
Some(value) => async_graphql::MaybeUndefined::Value(T2::from(value)),
None => async_graphql::MaybeUndefined::Null,
})
}
}

impl<T> Default for MaybeUndefined<T> {
fn default() -> Self {
Self(async_graphql::MaybeUndefined::Undefined)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test() {
assert_eq!(
MaybeUndefined::from(None::<bool>),
MaybeUndefined(async_graphql::MaybeUndefined::<bool>::Null)
);
assert_eq!(
MaybeUndefined::from(Some(true)),
MaybeUndefined(async_graphql::MaybeUndefined::Value(true))
);
assert_eq!(
MaybeUndefined::<bool>::default(),
MaybeUndefined(async_graphql::MaybeUndefined::Undefined)
);
}
}
12 changes: 12 additions & 0 deletions cynic/src/queries/flatten.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
// use crate::MaybeUndefined;

/// Encodes the rules for what types can be flattened into other types
/// via the `#[cynic(flatten)]` attribute.
///
/// This trait is sealed and can't be implemented by users of cynic.
pub trait FlattensInto<T>: private::Sealed<T> {}

impl<T> FlattensInto<Vec<T>> for Vec<Option<T>> {}

impl<T> FlattensInto<Vec<T>> for Option<Vec<T>> {}
impl<T> FlattensInto<Option<Vec<T>>> for Option<Vec<Option<T>>> {}
impl<T> FlattensInto<Vec<T>> for Option<Vec<Option<T>>> {}

// impl<T> FlattensInto<Vec<T>> for MaybeUndefined<Vec<T>> {}
// impl<T> FlattensInto<Option<Vec<T>>> for MaybeUndefined<Vec<Option<T>>> {}
// impl<T> FlattensInto<Vec<T>> for MaybeUndefined<Vec<Option<T>>> {}

mod private {
pub trait Sealed<T> {}

impl<T> Sealed<Vec<T>> for Vec<Option<T>> {}

impl<T> Sealed<Vec<T>> for Option<Vec<T>> {}
impl<T> Sealed<Option<Vec<T>>> for Option<Vec<Option<T>>> {}
impl<T> Sealed<Vec<T>> for Option<Vec<Option<T>>> {}

// impl<T> Sealed<Vec<T>> for MaybeUndefined<Vec<T>> {}
// impl<T> Sealed<Option<Vec<T>>> for MaybeUndefined<Vec<Option<T>>> {}
// impl<T> Sealed<Vec<T>> for MaybeUndefined<Vec<Option<T>>> {}
}
16 changes: 16 additions & 0 deletions cynic/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
//! usually be marker types and the associated types will also usually be
//! markers.

use crate::MaybeUndefined;

/// Indicates that a struct represents a Field in a graphql schema.
pub trait Field {
/// The schema marker type of this field.
Expand Down Expand Up @@ -74,6 +76,20 @@ where
type SchemaType = Option<U::SchemaType>;
}

impl<T, U> IsScalar<MaybeUndefined<T>> for MaybeUndefined<U>
where
U: IsScalar<T>,
{
type SchemaType = MaybeUndefined<U::SchemaType>;
}
Comment on lines +79 to +84
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this impl isn't needed


impl<T, U> IsScalar<Option<T>> for MaybeUndefined<U>
where
U: IsScalar<T>,
{
type SchemaType = Option<U::SchemaType>;
}

impl<T, U> IsScalar<Vec<T>> for Vec<U>
where
U: IsScalar<T>,
Expand Down
Loading