Skip to content

Commit d79b986

Browse files
committed
Statically assert input object fields
1 parent ea77f95 commit d79b986

File tree

8 files changed

+186
-42
lines changed

8 files changed

+186
-42
lines changed

juniper/src/macros/reflect.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ pub trait FieldMeta<S, const N: FieldName> {
329329
/// [1]: https://spec.graphql.org/October2021#sec-Language.Fields
330330
type TypeInfo;
331331

332-
/// [`Types`] of [GraphQL field's][1] return type.
332+
/// [`Type`] of [GraphQL field's][1] return type.
333333
///
334334
/// [1]: https://spec.graphql.org/October2021#sec-Language.Fields
335335
const TYPE: Type;
@@ -930,6 +930,51 @@ macro_rules! assert_field_arg_deprecable {
930930
};
931931
}
932932

933+
/// Statically asserts whether an input object [`Field`] represents a `Null`able type, so can be
934+
/// deprecated.
935+
///
936+
/// Must be used in conjunction with the check whether the [`Field`] has its default value set.
937+
#[doc(hidden)]
938+
#[macro_export]
939+
macro_rules! assert_input_field_deprecable {
940+
(
941+
$ty: ty,
942+
$scalar: ty,
943+
$field_name: expr $(,)?
944+
) => {
945+
const _: () = {
946+
const TY_NAME: &::core::primitive::str =
947+
<$ty as $crate::macros::reflect::BaseType<$scalar>>::NAME;
948+
const FIELD_NAME: &::core::primitive::str = $field_name;
949+
const FIELD_TY_NAME: &::core::primitive::str =
950+
<$ty as $crate::macros::reflect::FieldMeta<
951+
$scalar,
952+
{ $crate::checked_hash!(FIELD_NAME, $ty, $scalar) },
953+
>>::TYPE;
954+
const FIELD_WRAPPED_VAL: $crate::macros::reflect::WrappedValue =
955+
<$ty as $crate::macros::reflect::FieldMeta<
956+
$scalar,
957+
{ $crate::checked_hash!(FIELD_NAME, $ty, $scalar) },
958+
>>::WRAPPED_VALUE;
959+
960+
if FIELD_WRAPPED_VAL % 10 != 2 {
961+
const FIELD_TY: &::core::primitive::str =
962+
$crate::format_type!(FIELD_TY_NAME, FIELD_WRAPPED_VAL);
963+
const ERROR_MSG: &::core::primitive::str = $crate::const_concat!(
964+
"field `",
965+
FIELD_NAME,
966+
"` of `",
967+
TY_NAME,
968+
"` input object cannot be deprecated, because its type `",
969+
FIELD_TY,
970+
"` is neither `Null`able nor the default field value is specified",
971+
);
972+
::core::panic!("{}", ERROR_MSG);
973+
}
974+
};
975+
};
976+
}
977+
933978
/// Concatenates `const` [`str`](prim@str)s in a `const` context.
934979
#[macro_export]
935980
macro_rules! const_concat {
@@ -978,7 +1023,7 @@ macro_rules! checked_hash {
9781023
$field_name,
9791024
"` isn't implemented on `",
9801025
<$impl_ty as $crate::macros::reflect::BaseType<$scalar>>::NAME,
981-
"`."
1026+
"`"
9821027
);
9831028
::core::panic!("{}", MSG)
9841029
}

juniper_codegen/src/graphql_input_object/mod.rs

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
pub(crate) mod derive;
66

77
use proc_macro2::TokenStream;
8-
use quote::{ToTokens, format_ident, quote};
8+
use quote::{ToTokens, format_ident, quote, quote_spanned};
99
use syn::{
1010
ext::IdentExt as _,
1111
parse::{Parse, ParseStream},
@@ -17,7 +17,7 @@ use syn::{
1717
use crate::common::{
1818
Description, SpanContainer, default, deprecation, filter_attrs,
1919
parse::{
20-
ParseBufferExt as _,
20+
GenericsExt as _, ParseBufferExt as _,
2121
attr::{OptionExt as _, err},
2222
},
2323
rename, scalar,
@@ -428,6 +428,7 @@ impl ToTokens for Definition {
428428
self.impl_from_input_value_tokens().to_tokens(into);
429429
self.impl_to_input_value_tokens().to_tokens(into);
430430
self.impl_reflection_traits_tokens().to_tokens(into);
431+
self.impl_field_meta_tokens().to_tokens(into);
431432
}
432433
}
433434

@@ -441,19 +442,31 @@ impl Definition {
441442
fn impl_input_type_tokens(&self) -> TokenStream {
442443
let ident = &self.ident;
443444
let scalar = &self.scalar;
445+
let const_scalar = &self.scalar.default_ty();
444446

445447
let generics = self.impl_generics(false);
446448
let (impl_generics, _, where_clause) = generics.split_for_impl();
447449
let (_, ty_generics, _) = self.generics.split_for_impl();
448450

449-
let assert_fields_input_values = self.fields.iter().filter_map(|f| {
450-
let ty = &f.ty;
451+
let assert_fields_input_values = self.fields.iter().filter(|f| !f.ignored).map(|f| {
452+
let field_ty = &f.ty;
451453

452-
(!f.ignored).then(|| {
453-
quote! {
454-
<#ty as ::juniper::marker::IsInputType<#scalar>>::mark();
454+
let assert_deprecable = (f.deprecated.is_some() && f.default.is_none()).then(|| {
455+
let field_name = &f.name;
456+
457+
quote_spanned! { field_ty.span() =>
458+
::juniper::assert_input_field_deprecable!(
459+
#ident #ty_generics,
460+
#const_scalar,
461+
#field_name,
462+
);
455463
}
456-
})
464+
});
465+
466+
quote! {
467+
<#field_ty as ::juniper::marker::IsInputType<#scalar>>::mark();
468+
#assert_deprecable
469+
}
457470
});
458471

459472
quote! {
@@ -726,6 +739,8 @@ impl Definition {
726739
let (impl_generics, _, where_clause) = generics.split_for_impl();
727740
let (_, ty_generics, _) = self.generics.split_for_impl();
728741

742+
let fields = self.fields.iter().filter(|f| !f.ignored).map(|f| &f.name);
743+
729744
quote! {
730745
#[automatically_derived]
731746
impl #impl_generics ::juniper::macros::reflect::BaseType<#scalar>
@@ -735,6 +750,7 @@ impl Definition {
735750
const NAME: ::juniper::macros::reflect::Type = #name;
736751
}
737752

753+
#[automatically_derived]
738754
impl #impl_generics ::juniper::macros::reflect::BaseSubTypes<#scalar>
739755
for #ident #ty_generics
740756
#where_clause
@@ -743,15 +759,72 @@ impl Definition {
743759
&[<Self as ::juniper::macros::reflect::BaseType<#scalar>>::NAME];
744760
}
745761

762+
#[automatically_derived]
746763
impl #impl_generics ::juniper::macros::reflect::WrappedType<#scalar>
747764
for #ident #ty_generics
748765
#where_clause
749766
{
750767
const VALUE: ::juniper::macros::reflect::WrappedValue = 1;
751768
}
769+
770+
#[automatically_derived]
771+
impl #impl_generics ::juniper::macros::reflect::Fields<#scalar>
772+
for #ident #ty_generics
773+
#where_clause
774+
{
775+
const NAMES: ::juniper::macros::reflect::Names = &[#(#fields),*];
776+
}
752777
}
753778
}
754779

780+
/// Returns generated code implementing [`FieldMeta`] for each field of this
781+
/// [GraphQL input object][0].
782+
///
783+
/// [`FieldMeta`]: juniper::macros::reflect::FieldMeta
784+
/// [0]: https://spec.graphql.org/October2021#sec-Input-Objects
785+
fn impl_field_meta_tokens(&self) -> TokenStream {
786+
let ident = &self.ident;
787+
let context = &self.context;
788+
let scalar = &self.scalar;
789+
790+
let generics = self.impl_generics(false);
791+
let (impl_generics, _, where_clause) = generics.split_for_impl();
792+
let (_, ty_generics, _) = self.generics.split_for_impl();
793+
794+
self.fields
795+
.iter()
796+
.filter(|f| !f.ignored)
797+
.map(|field| {
798+
let field_name = &field.name;
799+
let mut field_ty = field.ty.clone();
800+
generics.replace_type_with_defaults(&mut field_ty);
801+
802+
quote! {
803+
#[allow(non_snake_case)]
804+
#[automatically_derived]
805+
impl #impl_generics ::juniper::macros::reflect::FieldMeta<
806+
#scalar,
807+
{ ::juniper::macros::reflect::fnv1a128(#field_name) }
808+
> for #ident #ty_generics #where_clause {
809+
type Context = #context;
810+
type TypeInfo = ();
811+
const TYPE: ::juniper::macros::reflect::Type =
812+
<#field_ty as ::juniper::macros::reflect::BaseType<#scalar>>::NAME;
813+
const SUB_TYPES: ::juniper::macros::reflect::Types =
814+
<#field_ty as ::juniper::macros::reflect::BaseSubTypes<#scalar>>::NAMES;
815+
const WRAPPED_VALUE: ::juniper::macros::reflect::WrappedValue =
816+
<#field_ty as ::juniper::macros::reflect::WrappedType<#scalar>>::VALUE;
817+
const ARGUMENTS: &'static [(
818+
::juniper::macros::reflect::Name,
819+
::juniper::macros::reflect::Type,
820+
::juniper::macros::reflect::WrappedValue,
821+
)] = &[];
822+
}
823+
}
824+
})
825+
.collect()
826+
}
827+
755828
/// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and
756829
/// similar) implementation of this struct.
757830
///
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use juniper::GraphQLInputObject;
2+
3+
#[derive(GraphQLInputObject)]
4+
struct Object {
5+
#[graphql(deprecated)]
6+
test: String,
7+
#[deprecated]
8+
other: i32,
9+
}
10+
11+
fn main() {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error[E0080]: evaluation panicked: field `test` of `Object` input object cannot be deprecated, because its type `String!` is neither `Null`able nor the default field value is specified
2+
--> fail/input-object/derive_field_non_deprecable.rs:6:11
3+
|
4+
6 | test: String,
5+
| ^^^^^^ evaluation of `<Object as juniper::marker::IsInputType<__S>>::mark::_` failed here
6+
|
7+
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_input_field_deprecable` (in Nightly builds, run with -Z macro-backtrace for more info)
8+
9+
error[E0080]: evaluation panicked: field `other` of `Object` input object cannot be deprecated, because its type `Int!` is neither `Null`able nor the default field value is specified
10+
--> fail/input-object/derive_field_non_deprecable.rs:8:12
11+
|
12+
8 | other: i32,
13+
| ^^^ evaluation of `<Object as juniper::marker::IsInputType<__S>>::mark::_` failed here
14+
|
15+
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_input_field_deprecable` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/codegen/fail/interface/struct/attr_missing_field.stderr

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
1+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
22
--> fail/interface/struct/attr_missing_field.rs:11:5
33
|
44
11 | id: String,
55
| ^^ evaluation of `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::IMPL_ARGS::{constant#0}` failed here
66
|
77
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_field` (in Nightly builds, run with -Z macro-backtrace for more info)
88

9-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
9+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
1010
--> fail/interface/struct/attr_missing_field.rs:11:5
1111
|
1212
11 | id: String,
1313
| ^^ evaluation of `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::IMPL_RETURN_TY::{constant#0}` failed here
1414
|
1515
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_field` (in Nightly builds, run with -Z macro-backtrace for more info)
1616

17-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
17+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
1818
--> fail/interface/struct/attr_missing_field.rs:11:5
1919
|
2020
11 | id: String,
2121
| ^^ evaluation of `<CharacterValueEnum<ObjA> as juniper::macros::reflect::AsyncField<__S, id>>::call::_::IMPL_ARGS::{constant#0}` failed here
2222
|
2323
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_field` (in Nightly builds, run with -Z macro-backtrace for more info)
2424

25-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
25+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
2626
--> fail/interface/struct/attr_missing_field.rs:11:5
2727
|
2828
11 | id: String,
@@ -39,7 +39,7 @@ error[E0277]: the trait bound `ObjA: reflect::Field<__S, 11301463986558097276003
3939
= help: the trait `Field<__S, 11301463986558097276003903130001171064>` is not implemented for `ObjA`
4040
but trait `Field<__S, 140650918148392961738240285796466530725>` is implemented for it
4141

42-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
42+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
4343
--> fail/interface/struct/attr_missing_field.rs:11:5
4444
|
4545
11 | id: String,
@@ -56,7 +56,7 @@ error[E0277]: the trait bound `ObjA: AsyncField<__S, 113014639865580972760039031
5656
= help: the trait `AsyncField<__S, 11301463986558097276003903130001171064>` is not implemented for `ObjA`
5757
but trait `AsyncField<__S, 140650918148392961738240285796466530725>` is implemented for it
5858

59-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
59+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
6060
--> fail/interface/struct/attr_missing_field.rs:11:5
6161
|
6262
11 | id: String,

tests/codegen/fail/interface/struct/derive_missing_field.stderr

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
1+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
22
--> fail/interface/struct/derive_missing_field.rs:12:5
33
|
44
12 | id: String,
55
| ^^ evaluation of `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::IMPL_ARGS::{constant#0}` failed here
66
|
77
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_field` (in Nightly builds, run with -Z macro-backtrace for more info)
88

9-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
9+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
1010
--> fail/interface/struct/derive_missing_field.rs:12:5
1111
|
1212
12 | id: String,
1313
| ^^ evaluation of `<CharacterValueEnum<ObjA> as juniper::macros::reflect::Field<__S, id>>::call::_::IMPL_RETURN_TY::{constant#0}` failed here
1414
|
1515
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_field` (in Nightly builds, run with -Z macro-backtrace for more info)
1616

17-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
17+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
1818
--> fail/interface/struct/derive_missing_field.rs:12:5
1919
|
2020
12 | id: String,
2121
| ^^ evaluation of `<CharacterValueEnum<ObjA> as juniper::macros::reflect::AsyncField<__S, id>>::call::_::IMPL_ARGS::{constant#0}` failed here
2222
|
2323
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `::juniper::assert_field` (in Nightly builds, run with -Z macro-backtrace for more info)
2424

25-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
25+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
2626
--> fail/interface/struct/derive_missing_field.rs:12:5
2727
|
2828
12 | id: String,
@@ -39,7 +39,7 @@ error[E0277]: the trait bound `ObjA: reflect::Field<__S, 11301463986558097276003
3939
= help: the trait `Field<__S, 11301463986558097276003903130001171064>` is not implemented for `ObjA`
4040
but trait `Field<__S, 140650918148392961738240285796466530725>` is implemented for it
4141

42-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
42+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
4343
--> fail/interface/struct/derive_missing_field.rs:12:5
4444
|
4545
12 | id: String,
@@ -56,7 +56,7 @@ error[E0277]: the trait bound `ObjA: AsyncField<__S, 113014639865580972760039031
5656
= help: the trait `AsyncField<__S, 11301463986558097276003903130001171064>` is not implemented for `ObjA`
5757
but trait `AsyncField<__S, 140650918148392961738240285796466530725>` is implemented for it
5858

59-
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`.
59+
error[E0080]: evaluation panicked: Failed to implement interface `Character` on `ObjA`: field `id` isn't implemented on `ObjA`
6060
--> fail/interface/struct/derive_missing_field.rs:12:5
6161
|
6262
12 | id: String,

0 commit comments

Comments
 (0)