From 8be4da104ea46adaa1bd16a82dfcd2073e77220b Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 9 Oct 2025 17:34:04 +0300 Subject: [PATCH 1/9] Bootstrap --- juniper/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index b03e1c9d1..8918cbf2f 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -36,6 +36,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Support for variable-length escaped Unicode characters (e.g. `\u{110000}`) in strings. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687]) - Full Unicode range support. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687]) - Support parsing descriptions on operations, fragments and variable definitions. ([#1349], [graphql/graphql-spec#1170]) + - ??? - Support for [block strings][0180-1]. ([#1349]) ### Changed From 34576506a8178873968bbae37dffb325bec0c9f9 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 9 Oct 2025 17:59:31 +0300 Subject: [PATCH 2/9] Support `__Type.isOneOf` field in schema --- juniper/CHANGELOG.md | 6 ++++-- juniper/src/schema/meta.rs | 12 ++++++++++++ juniper/src/schema/schema.rs | 3 +-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 8918cbf2f..952e7a936 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -24,7 +24,9 @@ All user visible changes to `juniper` crate will be documented in this file. Thi ### Added - [September 2025] GraphQL spec: ([#1347]) - - `__Type.isOneOf` field. ([#1348], [graphql/graphql-spec#825]) + - `@oneOf` input objects: ([#1354], [graphql/graphql-spec#825]) + - `__Type.isOneOf` field. ([#1348]) + - `schema::meta::InputObjectMeta::is_one_of` field. - `SCHEMA`, `OBJECT`, `ARGUMENT_DEFINITION`, `INTERFACE`, `UNION`, `ENUM`, `INPUT_OBJECT` and `INPUT_FIELD_DEFINITION` values to `__DirectiveLocation` enum. ([#1348]) - Arguments and input object fields deprecation: ([#1348], [#864], [graphql/graphql-spec#525], [graphql/graphql-spec#805]) - Placing `#[graphql(deprecated)]` and `#[deprecated]` attributes on struct fields in `#[derive(GraphQLInputObject)]` macro. @@ -36,7 +38,6 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - Support for variable-length escaped Unicode characters (e.g. `\u{110000}`) in strings. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687]) - Full Unicode range support. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687]) - Support parsing descriptions on operations, fragments and variable definitions. ([#1349], [graphql/graphql-spec#1170]) - - ??? - Support for [block strings][0180-1]. ([#1349]) ### Changed @@ -55,6 +56,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi [#1348]: /../../pull/1348 [#1349]: /../../pull/1349 [#1353]: /../../pull/1353 +[#1354]: /../../pull/1354 [graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525 [graphql/graphql-spec#687]: https://github.com/graphql/graphql-spec/issues/687 [graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805 diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index 27f44711d..8437c90c7 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -348,6 +348,8 @@ pub struct InputObjectMeta { pub description: Option, #[doc(hidden)] pub input_fields: Vec>, + #[doc(hidden)] + pub is_one_of: bool, #[debug(ignore)] pub(crate) try_parse_fn: InputValueParseFn, } @@ -364,6 +366,7 @@ impl InputObjectMeta { name: name.into(), description: None, input_fields: input_fields.to_vec(), + is_one_of: false, try_parse_fn: try_parse_fn::, } } @@ -377,6 +380,15 @@ impl InputObjectMeta { self } + /// Marks this [`InputObjectMeta`] type as [`@oneOf`]. + /// + /// [`@oneOf`]: https://spec.graphql.org/September2025#sec--oneOf + #[must_use] + pub fn one_of(mut self) -> Self { + self.is_one_of = true; + self + } + /// Wraps this [`InputObjectMeta`] type into a generic [`MetaType`]. pub fn into_meta(self) -> MetaType { MetaType::InputObject(self) diff --git a/juniper/src/schema/schema.rs b/juniper/src/schema/schema.rs index 39313a7ff..b3cc91bb6 100644 --- a/juniper/src/schema/schema.rs +++ b/juniper/src/schema/schema.rs @@ -375,8 +375,7 @@ impl<'a, S: ScalarValue + 'a> TypeType<'a, S> { fn is_one_of(&self) -> Option { match self { Self::Concrete(t) => match t { - // TODO: Implement once `@oneOf` is implemented for input objects. - MetaType::InputObject(InputObjectMeta { .. }) => Some(false), + MetaType::InputObject(InputObjectMeta { is_one_of, .. }) => Some(*is_one_of), MetaType::Enum(..) | MetaType::Interface(..) | MetaType::List(..) From b07da73e636530440b4b5eed29b9d7c98a7c74c9 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 9 Oct 2025 18:06:47 +0300 Subject: [PATCH 3/9] Add `@oneOf` built-in directive --- juniper/CHANGELOG.md | 1 + juniper/src/schema/model.rs | 54 +++++++++++++++++++++++-------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 952e7a936..f428152ee 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -25,6 +25,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi - [September 2025] GraphQL spec: ([#1347]) - `@oneOf` input objects: ([#1354], [graphql/graphql-spec#825]) + - `@oneOf` built-in directive. - `__Type.isOneOf` field. ([#1348]) - `schema::meta::InputObjectMeta::is_one_of` field. - `SCHEMA`, `OBJECT`, `ARGUMENT_DEFINITION`, `INTERFACE`, `UNION`, `ENUM`, `INPUT_OBJECT` and `INPUT_FIELD_DEFINITION` values to `__DirectiveLocation` enum. ([#1348]) diff --git a/juniper/src/schema/model.rs b/juniper/src/schema/model.rs index 95e973356..eaa6adeed 100644 --- a/juniper/src/schema/model.rs +++ b/juniper/src/schema/model.rs @@ -250,14 +250,16 @@ impl SchemaType { registry.get_type::>(&()); + let deprecated_directive = DirectiveType::new_deprecated(&mut registry); let include_directive = DirectiveType::new_include(&mut registry); + let one_of_directive = DirectiveType::new_one_of(); let skip_directive = DirectiveType::new_skip(&mut registry); - let deprecated_directive = DirectiveType::new_deprecated(&mut registry); let specified_by_directive = DirectiveType::new_specified_by(&mut registry); directives.insert(include_directive.name.clone(), include_directive); directives.insert(skip_directive.name.clone(), skip_directive); directives.insert(deprecated_directive.name.clone(), deprecated_directive); directives.insert(specified_by_directive.name.clone(), specified_by_directive); + directives.insert(one_of_directive.name.clone(), one_of_directive); let mut meta_fields = vec![ registry.field::>(arcstr::literal!("__schema"), &()), @@ -585,28 +587,33 @@ impl DirectiveType { } } - fn new_include(registry: &mut Registry) -> Self + fn new_deprecated(registry: &mut Registry) -> Self where S: ScalarValue, { Self::new( - arcstr::literal!("include"), + arcstr::literal!("deprecated"), &[ - DirectiveLocation::Field, - DirectiveLocation::FragmentSpread, - DirectiveLocation::InlineFragment, + DirectiveLocation::FieldDefinition, + DirectiveLocation::ArgumentDefinition, + DirectiveLocation::InputFieldDefinition, + DirectiveLocation::EnumValue, ], - &[registry.arg::(arcstr::literal!("if"), &())], + &[registry.arg_with_default::( + arcstr::literal!("reason"), + &"No longer supported".into(), + &(), + )], false, ) } - fn new_skip(registry: &mut Registry) -> Self + fn new_include(registry: &mut Registry) -> Self where S: ScalarValue, { Self::new( - arcstr::literal!("skip"), + arcstr::literal!("include"), &[ DirectiveLocation::Field, DirectiveLocation::FragmentSpread, @@ -617,23 +624,30 @@ impl DirectiveType { ) } - fn new_deprecated(registry: &mut Registry) -> Self + fn new_one_of() -> Self where S: ScalarValue, { Self::new( - arcstr::literal!("deprecated"), + arcstr::literal!("oneOf"), + &[DirectiveLocation::InputObject], + &[], + false, + ) + } + + fn new_skip(registry: &mut Registry) -> Self + where + S: ScalarValue, + { + Self::new( + arcstr::literal!("skip"), &[ - DirectiveLocation::FieldDefinition, - DirectiveLocation::ArgumentDefinition, - DirectiveLocation::InputFieldDefinition, - DirectiveLocation::EnumValue, + DirectiveLocation::Field, + DirectiveLocation::FragmentSpread, + DirectiveLocation::InlineFragment, ], - &[registry.arg_with_default::( - arcstr::literal!("reason"), - &"No longer supported".into(), - &(), - )], + &[registry.arg::(arcstr::literal!("if"), &())], false, ) } From 3fd7bbda394c687f4c91a19ebc3c0ae37663a546 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 9 Oct 2025 18:21:56 +0300 Subject: [PATCH 4/9] Place `@oneOf` directive when generating schema --- .../src/schema/translate/graphql_parser.rs | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/juniper/src/schema/translate/graphql_parser.rs b/juniper/src/schema/translate/graphql_parser.rs index c687d029a..6c2fb8f26 100644 --- a/juniper/src/schema/translate/graphql_parser.rs +++ b/juniper/src/schema/translate/graphql_parser.rs @@ -83,7 +83,9 @@ impl GraphQLParserTranslator { default_value: default_value .as_ref() .map(|x| GraphQLParserTranslator::translate_value(x)), - directives: generate_directives(deprecation_status), + directives: deprecation_directive(deprecation_status) + .map(|d| vec![d]) + .unwrap_or_default(), } } @@ -159,7 +161,7 @@ impl GraphQLParserTranslator { name: name.as_str().into(), directives: specified_by_url .as_deref() - .map(|url| vec![specified_by_url_to_directive(url)]) + .map(|url| vec![specified_by_url_directive(url)]) .unwrap_or_default(), }), meta::MetaType::Enum(meta::EnumMeta { @@ -209,12 +211,15 @@ impl GraphQLParserTranslator { name, description, input_fields, + is_one_of, try_parse_fn: _, }) => schema::TypeDefinition::InputObject(schema::InputObjectType { position: Pos::default(), description: description.as_deref().map(Into::into), name: name.as_str().into(), - directives: vec![], + directives: is_one_of + .then(|| vec![one_of_directive()]) + .unwrap_or_default(), fields: input_fields .iter() .filter(|x| !x.is_builtin()) @@ -255,7 +260,9 @@ impl GraphQLParserTranslator { position: Pos::default(), name: name.as_str().into(), description: description.as_deref().map(Into::into), - directives: generate_directives(deprecation_status), + directives: deprecation_directive(deprecation_status) + .map(|d| vec![d]) + .unwrap_or_default(), } } @@ -275,7 +282,9 @@ impl GraphQLParserTranslator { position: Pos::default(), name: name.as_str().into(), description: description.as_deref().map(Into::into), - directives: generate_directives(deprecation_status), + directives: deprecation_directive(deprecation_status) + .map(|d| vec![d]) + .unwrap_or_default(), field_type: GraphQLParserTranslator::translate_type(field_type), arguments: arguments .as_ref() @@ -290,7 +299,11 @@ impl GraphQLParserTranslator { } } -fn deprecation_to_directive<'a, T>( +/// Forms a [`@deprecated(reason:)`] [`schema::Directive`] out of the provided +/// [`meta::DeprecationStatus`]. +/// +/// [`@deprecated(reason:)`]: https://spec.graphql.org/September2025#sec--deprecated +fn deprecation_directive<'a, T>( status: &meta::DeprecationStatus, ) -> Option> where @@ -309,28 +322,32 @@ where } } -/// Returns the `@specifiedBy(url:)` [`schema::Directive`] for the provided `url`. -fn specified_by_url_to_directive<'a, T>(url: &str) -> schema::Directive<'a, T> +/// Forms a [`@oneOf`] [`schema::Directive`]. +/// +/// [`@oneOf`]: https://spec.graphql.org/September2025#sec--oneOf +fn one_of_directive<'a, T>() -> schema::Directive<'a, T> where T: schema::Text<'a>, { schema::Directive { position: Pos::default(), - name: "specifiedBy".into(), - arguments: vec![("url".into(), schema::Value::String(url.into()))], + name: "oneOf".into(), + arguments: vec![], } } -// Right now the only directive supported is `@deprecated`. -// `@skip` and `@include` are dealt with elsewhere. -// https://spec.graphql.org/October2021#sec-Type-System.Directives.Built-in-Directives -fn generate_directives<'a, T>(status: &meta::DeprecationStatus) -> Vec> +/// Forms a `@specifiedBy(url:)` [`schema::Directive`] out of the provided `url`. +/// +/// [`@specifiedBy(url:)`]: https://spec.graphql.org/September2025#sec--specifiedBy +fn specified_by_url_directive<'a, T>(url: &str) -> schema::Directive<'a, T> where T: schema::Text<'a>, { - deprecation_to_directive(status) - .map(|d| vec![d]) - .unwrap_or_default() + schema::Directive { + position: Pos::default(), + name: "specifiedBy".into(), + arguments: vec![("url".into(), schema::Value::String(url.into()))], + } } /// Sorts the provided [`schema::Document`] in the "type-then-name" manner. From 7e68c6c302a22fac5d9a7913dcbc09ac09cb1855 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 13 Oct 2025 13:15:51 +0300 Subject: [PATCH 5/9] Parse enums in `derive(GraphQLInputObject)` --- .../src/graphql_input_object/derive.rs | 99 +++++++++++++++---- .../src/graphql_input_object/mod.rs | 6 ++ 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/juniper_codegen/src/graphql_input_object/derive.rs b/juniper_codegen/src/graphql_input_object/derive.rs index f7137aedf..825c85ed4 100644 --- a/juniper_codegen/src/graphql_input_object/derive.rs +++ b/juniper_codegen/src/graphql_input_object/derive.rs @@ -13,39 +13,47 @@ use super::{ContainerAttr, Definition, FieldAttr, FieldDefinition}; /// [`diagnostic::Scope`] of errors for `#[derive(GraphQLInputObject)]` macro. const ERR: diagnostic::Scope = diagnostic::Scope::InputObjectDerive; -/// Expands `#[derive(GraphQLInputObject)]` macro into generated code. +/// Expands `#[derive(GraphQLInputObject)]` macro placed on a struct or an enum. pub fn expand(input: TokenStream) -> syn::Result { let ast = syn::parse2::(input)?; let attr = ContainerAttr::from_attrs("graphql", &ast.attrs)?; - let syn::Data::Struct(data) = &ast.data else { - return Err(ERR.custom_error(ast.span(), "can only be derived on structs")); - }; - let renaming = attr .rename_fields .map(SpanContainer::into_inner) .unwrap_or(rename::Policy::CamelCase); - let is_internal = attr.is_internal; - let fields = data - .fields - .iter() - .filter_map(|f| parse_field(f, renaming, is_internal)) - .collect::>(); + let (fields, fields_span) = match &ast.data { + syn::Data::Struct(data) => { + let fields = data + .fields + .iter() + .filter_map(|f| parse_struct_field(f, renaming, is_internal)) + .collect::>(); + (fields, data.fields.span()) + } + syn::Data::Enum(data) => { + let fields = data + .variants + .iter() + .filter_map(|v| parse_enum_variant(v, renaming, is_internal)) + .collect::>(); + (fields, data.variants.span()) + } + syn::Data::Union(_) => { + return Err(ERR.custom_error(ast.span(), "cannot be derived on unions")); + } + }; diagnostic::abort_if_dirty(); if !fields.iter().any(|f| !f.ignored) { - return Err(ERR.custom_error(data.fields.span(), "expected at least 1 non-ignored field")); + return Err(ERR.custom_error(fields_span, "expected at least 1 non-ignored field")); } let unique_fields = fields.iter().map(|v| &v.name).collect::>(); if unique_fields.len() != fields.len() { - return Err(ERR.custom_error( - data.fields.span(), - "expected all fields to have unique names", - )); + return Err(ERR.custom_error(fields_span, "expected all fields to have unique names")); } let name = attr @@ -79,15 +87,16 @@ pub fn expand(input: TokenStream) -> syn::Result { context, scalar, fields, + is_one_of: matches!(ast.data, syn::Data::Enum(_)), }; Ok(definition.into_token_stream()) } -/// Parses a [`FieldDefinition`] from the given struct field definition. +/// Parses a [`FieldDefinition`] from the provided struct field definition. /// /// Returns [`None`] if the parsing fails. -fn parse_field( +fn parse_struct_field( f: &syn::Field, renaming: rename::Policy, is_internal: bool, @@ -120,8 +129,58 @@ fn parse_field( }) } -/// Emits "expected named struct field" [`syn::Error`] pointing to the given -/// `span`. +/// Parses a [`FieldDefinition`] from the provided enum variant definition. +/// +/// Returns [`None`] if the parsing fails. +fn parse_enum_variant( + v: &syn::Variant, + renaming: rename::Policy, + is_internal: bool, +) -> Option { + if v.fields.len() != 1 || !matches!(v.fields, syn::Fields::Unnamed(_)) { + ERR.emit_custom( + v.fields.span(), + "enum variant must have exactly 1 unnamed field to represent `@oneOf` input object \ + field", + ); + } + + let field_attr = FieldAttr::from_attrs("graphql", &v.attrs) + .map_err(diagnostic::emit_error) + .ok()?; + + if let Some(default) = &field_attr.default { + ERR.emit_custom( + default.span_ident(), + "field of `@oneOf` input object cannot have default value", + ); + } + + let ident = &v.ident; + + let name = field_attr + .name + .map_or_else( + || renaming.apply(&ident.unraw().to_string()), + SpanContainer::into_inner, + ) + .into_boxed_str(); + if !is_internal && name.starts_with("__") { + ERR.no_double_underscore(v.span()); + } + + Some(FieldDefinition { + ident: ident.clone(), + ty: v.fields.iter().next().unwrap().ty.clone(), + default: None, + name, + description: field_attr.description.map(SpanContainer::into_inner), + deprecated: field_attr.deprecated.map(SpanContainer::into_inner), + ignored: field_attr.ignore.is_some(), + }) +} + +/// Emits "expected named struct field" [`syn::Error`] pointing to the provided `span`. pub(crate) fn err_unnamed_field(span: &S) -> Option { ERR.emit_custom(span.span(), "expected named struct field"); None diff --git a/juniper_codegen/src/graphql_input_object/mod.rs b/juniper_codegen/src/graphql_input_object/mod.rs index 48b9e00f6..e357980aa 100644 --- a/juniper_codegen/src/graphql_input_object/mod.rs +++ b/juniper_codegen/src/graphql_input_object/mod.rs @@ -417,6 +417,12 @@ struct Definition { /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects /// [1]: https://spec.graphql.org/October2021#InputFieldsDefinition fields: Vec, + + /// Indicator whether this [GraphQL input object][0] is [`@oneOf`]. + /// + /// [`@oneOf`]: https://spec.graphql.org/September2025#oneof-input-object + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + is_one_of: bool, } impl ToTokens for Definition { From fd7a6517918f8fc66051c99e429f965b1e6b952d Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 13 Oct 2025 14:49:20 +0300 Subject: [PATCH 6/9] Codegen for enums in `derive(GraphQLInputObject)` --- juniper_codegen/src/graphql_enum/mod.rs | 2 +- .../src/graphql_input_object/derive.rs | 4 +- .../src/graphql_input_object/mod.rs | 143 ++++++++++++------ 3 files changed, 99 insertions(+), 50 deletions(-) diff --git a/juniper_codegen/src/graphql_enum/mod.rs b/juniper_codegen/src/graphql_enum/mod.rs index b8abf3a44..be4fa0491 100644 --- a/juniper_codegen/src/graphql_enum/mod.rs +++ b/juniper_codegen/src/graphql_enum/mod.rs @@ -633,7 +633,7 @@ impl Definition { let ignored = self.has_ignored_variants.then(|| { quote! { - _ => ::core::panic!("Cannot resolve ignored enum variant"), + _ => ::core::panic!("cannot resolve ignored enum variant"), } }); diff --git a/juniper_codegen/src/graphql_input_object/derive.rs b/juniper_codegen/src/graphql_input_object/derive.rs index 825c85ed4..27824ffe9 100644 --- a/juniper_codegen/src/graphql_input_object/derive.rs +++ b/juniper_codegen/src/graphql_input_object/derive.rs @@ -169,9 +169,11 @@ fn parse_enum_variant( ERR.no_double_underscore(v.span()); } + let field_ty = v.fields.iter().next().unwrap().ty.clone(); + Some(FieldDefinition { ident: ident.clone(), - ty: v.fields.iter().next().unwrap().ty.clone(), + ty: parse_quote! { ::core::option::Option<#field_ty> }, default: None, name, description: field_attr.description.map(SpanContainer::into_inner), diff --git a/juniper_codegen/src/graphql_input_object/mod.rs b/juniper_codegen/src/graphql_input_object/mod.rs index e357980aa..f56eb0e5c 100644 --- a/juniper_codegen/src/graphql_input_object/mod.rs +++ b/juniper_codegen/src/graphql_input_object/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod derive; use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote, quote_spanned}; +use std::iter; use syn::{ ext::IdentExt as _, parse::{Parse, ParseStream}, @@ -505,6 +506,8 @@ impl Definition { let description = &self.description; + let one_of = self.is_one_of.then(|| quote! { .one_of() }); + let fields = self.fields.iter().filter_map(|f| { let ty = &f.ty; let name = &f.name; @@ -546,6 +549,7 @@ impl Definition { registry .build_input_object_type::<#ident #ty_generics>(info, &fields) #description + #one_of .into_meta() } } @@ -623,49 +627,87 @@ impl Definition { let (impl_generics, _, where_clause) = generics.split_for_impl(); let (_, ty_generics, _) = self.generics.split_for_impl(); - let fields = self.fields.iter().map(|f| { - let ident = &f.ident; + let body = if self.is_one_of { + let variants = self + .fields + .iter() + .filter(|f| !f.ignored) + .collect::>(); + let fields_names = variants.iter().map(|f| &f.name); - let construct = if f.ignored { - f.default.as_ref().map_or_else( - || { - let expr = default::Value::default(); - quote! { #expr } - }, - |expr| quote! { #expr }, - ) - } else { - let name = &f.name; + let some_pat = quote! { ::core::option::Option::Some(v) }; + let none_pat = quote! { ::core::option::Option::None }; - let fallback = f.default.as_ref().map_or_else( - || { - quote! { - ::juniper::FromInputValue::<#scalar>::from_implicit_null() - .map_err(::juniper::IntoFieldError::into_field_error)? - } - }, - |expr| quote! { #expr }, - ); + let arms = variants.iter().enumerate().map(|(n, v)| { + let variant_ident = &v.ident; + + let pre_none_pats = iter::repeat(&none_pat).take(n); + let post_none_pats = iter::repeat(&none_pat).take(variants.len() - n - 1); quote! { - match obj.get(#name) { - ::core::option::Option::Some(v) => { + (#( #pre_none_pats, )* #some_pat, #( #post_none_pats, )*) => { + Self::#variant_ident( ::juniper::FromInputValue::<#scalar>::from_input_value(v) .map_err(::juniper::IntoFieldError::into_field_error)? - } - ::core::option::Option::None => { #fallback } + ) } } - }; + }); - quote! { #ident: { #construct }, } - }); + quote! { + match (#( obj.get(#fields_names), )*) { + #( #arms ) + _ => return Err(::juniper::FieldError::<#scalar>::from( + "Exactly one key must be specified", + )), + } + } + } else { + let fields = self.fields.iter().map(|f| { + let ident = &f.ident; + + let construct = if f.ignored { + f.default.as_ref().map_or_else( + || { + let expr = default::Value::default(); + quote! { #expr } + }, + |expr| quote! { #expr }, + ) + } else { + let name = &f.name; + + let fallback = f.default.as_ref().map_or_else( + || { + quote! { + ::juniper::FromInputValue::<#scalar>::from_implicit_null() + .map_err(::juniper::IntoFieldError::into_field_error)? + } + }, + |expr| quote! { #expr }, + ); + + quote! { + match obj.get(#name) { + ::core::option::Option::Some(v) => { + ::juniper::FromInputValue::<#scalar>::from_input_value(v) + .map_err(::juniper::IntoFieldError::into_field_error)? + } + ::core::option::Option::None => { #fallback } + } + } + }; + + quote! { #ident: #construct, } + }); + + quote! { Self { #( #fields )* } } + }; quote! { #[automatically_derived] - impl #impl_generics ::juniper::FromInputValue<#scalar> - for #ident #ty_generics - #where_clause + impl #impl_generics ::juniper::FromInputValue<#scalar> for #ident #ty_generics + #where_clause { type Error = ::juniper::FieldError<#scalar>; @@ -678,9 +720,7 @@ impl Definition { ::std::format!("Expected input object, found: {}", value)) )?; - ::core::result::Result::Ok(#ident { - #( #fields )* - }) + ::core::result::Result::Ok(#body) } } } @@ -700,36 +740,43 @@ impl Definition { let (impl_generics, _, where_clause) = generics.split_for_impl(); let (_, ty_generics, _) = self.generics.split_for_impl(); - let fields = self.fields.iter().filter_map(|f| { + let fields = self.fields.iter().filter(|&f| !f.ignored).map(|f| { let ident = &f.ident; let name = &f.name; - (!f.ignored).then(|| { + let value_expr = if self.is_one_of { quote! { - (#name, ::juniper::ToInputValue::to_input_value(&self.#ident)) + if let Self::#ident(v) = self { + ::core::option::Option::Some(v) + } else { + ::core::option::Option::None + } } - }) + } else { + quote! { self.#ident } + }; + + quote! { + (#name, ::juniper::ToInputValue::to_input_value(&#value_expr)) + } }); quote! { #[automatically_derived] - impl #impl_generics ::juniper::ToInputValue<#scalar> - for #ident #ty_generics - #where_clause + impl #impl_generics ::juniper::ToInputValue<#scalar> for #ident #ty_generics + #where_clause { fn to_input_value(&self) -> ::juniper::InputValue<#scalar> { ::juniper::InputValue::object( - #[allow(deprecated)] - ::std::array::IntoIter::new([#( #fields ),*]) - .collect() + ::core::iter::IntoIterator::into_iter([#( #fields ),*]).collect() ) } } } } - /// Returns generated code implementing [`BaseType`], [`BaseSubTypes`] and - /// [`WrappedType`] traits for this [GraphQL input object][0]. + /// Returns generated code implementing [`BaseType`], [`BaseSubTypes`] and [`WrappedType`] + /// traits for this [GraphQL input object][0]. /// /// [`BaseSubTypes`]: juniper::macros::reflect::BaseSubTypes /// [`BaseType`]: juniper::macros::reflect::BaseType @@ -745,7 +792,7 @@ impl Definition { let (impl_generics, _, where_clause) = generics.split_for_impl(); let (_, ty_generics, _) = self.generics.split_for_impl(); - let fields = self.fields.iter().filter(|f| !f.ignored).map(|f| &f.name); + let fields_names = self.fields.iter().filter(|f| !f.ignored).map(|f| &f.name); quote! { #[automatically_derived] @@ -778,7 +825,7 @@ impl Definition { for #ident #ty_generics #where_clause { - const NAMES: ::juniper::macros::reflect::Names = &[#(#fields),*]; + const NAMES: ::juniper::macros::reflect::Names = &[#(#fields_names),*]; } } } From 3cb4670024d07c7b44af442a36541482b15f3453 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 13 Oct 2025 16:01:10 +0300 Subject: [PATCH 7/9] Upd integration tests for struct input objects --- juniper_codegen/src/common/rename.rs | 51 +++-- .../src/graphql_input_object/derive.rs | 5 +- .../src/graphql_input_object/mod.rs | 2 +- ... => codegen_input_object_derive_struct.rs} | 204 ++++++++++-------- 4 files changed, 155 insertions(+), 107 deletions(-) rename tests/integration/tests/{codegen_input_object_derive.rs => codegen_input_object_derive_struct.rs} (86%) diff --git a/juniper_codegen/src/common/rename.rs b/juniper_codegen/src/common/rename.rs index d7c0cac9b..26f31462d 100644 --- a/juniper_codegen/src/common/rename.rs +++ b/juniper_codegen/src/common/rename.rs @@ -17,6 +17,9 @@ pub(crate) enum Policy { /// Rename in `camelCase` style. CamelCase, + /// Rename in `snake_case` style. + SnakeCase, + /// Rename in `SCREAMING_SNAKE_CASE` style. ScreamingSnakeCase, } @@ -27,7 +30,8 @@ impl Policy { match self { Self::None => name.into(), Self::CamelCase => to_camel_case(name), - Self::ScreamingSnakeCase => to_upper_snake_case(name), + Self::SnakeCase => to_snake_case(name, false), + Self::ScreamingSnakeCase => to_snake_case(name, true), } } } @@ -39,6 +43,7 @@ impl FromStr for Policy { match rule { "none" => Ok(Self::None), "camelCase" => Ok(Self::CamelCase), + "snake_case" => Ok(Self::SnakeCase), "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase), _ => Err(()), } @@ -97,9 +102,9 @@ fn to_camel_case(s: &str) -> String { dest } -fn to_upper_snake_case(s: &str) -> String { +fn to_snake_case(s: &str, upper: bool) -> String { let mut last_lower = false; - let mut upper = String::new(); + let mut out = String::new(); for c in s.chars() { if c == '_' { last_lower = false; @@ -107,16 +112,22 @@ fn to_upper_snake_case(s: &str) -> String { last_lower = true; } else if c.is_uppercase() { if last_lower { - upper.push('_'); + out.push('_'); } last_lower = false; } - for u in c.to_uppercase() { - upper.push(u); + if upper { + for u in c.to_uppercase() { + out.push(u); + } + } else { + for u in c.to_lowercase() { + out.push(u); + } } } - upper + out } #[cfg(test)] @@ -124,7 +135,7 @@ mod to_camel_case_tests { use super::to_camel_case; #[test] - fn converts_correctly() { + fn camel() { for (input, expected) in [ ("test", "test"), ("_test", "test"), @@ -143,11 +154,11 @@ mod to_camel_case_tests { } #[cfg(test)] -mod to_upper_snake_case_tests { - use super::to_upper_snake_case; +mod to_snake_case_tests { + use super::to_snake_case; #[test] - fn converts_correctly() { + fn upper() { for (input, expected) in [ ("abc", "ABC"), ("a_bc", "A_BC"), @@ -158,7 +169,23 @@ mod to_upper_snake_case_tests { ("someINpuT", "SOME_INPU_T"), ("some_INpuT", "SOME_INPU_T"), ] { - assert_eq!(to_upper_snake_case(input), expected); + assert_eq!(to_snake_case(input, true), expected); + } + } + + #[test] + fn lower() { + for (input, expected) in [ + ("abc", "abc"), + ("a_bc", "a_bc"), + ("ABC", "abc"), + ("A_BC", "a_bc"), + ("SomeInput", "some_input"), + ("someInput", "some_input"), + ("someINpuT", "some_inpu_t"), + ("some_INpuT", "some_inpu_t"), + ] { + assert_eq!(to_snake_case(input, false), expected); } } } diff --git a/juniper_codegen/src/graphql_input_object/derive.rs b/juniper_codegen/src/graphql_input_object/derive.rs index 27824ffe9..f38825c3d 100644 --- a/juniper_codegen/src/graphql_input_object/derive.rs +++ b/juniper_codegen/src/graphql_input_object/derive.rs @@ -161,7 +161,10 @@ fn parse_enum_variant( let name = field_attr .name .map_or_else( - || renaming.apply(&ident.unraw().to_string()), + || { + let name = rename::Policy::SnakeCase.apply(&ident.unraw().to_string()); + renaming.apply(&name) + }, SpanContainer::into_inner, ) .into_boxed_str(); diff --git a/juniper_codegen/src/graphql_input_object/mod.rs b/juniper_codegen/src/graphql_input_object/mod.rs index f56eb0e5c..c3c97f2cf 100644 --- a/juniper_codegen/src/graphql_input_object/mod.rs +++ b/juniper_codegen/src/graphql_input_object/mod.rs @@ -656,7 +656,7 @@ impl Definition { quote! { match (#( obj.get(#fields_names), )*) { - #( #arms ) + #( #arms )* _ => return Err(::juniper::FieldError::<#scalar>::from( "Exactly one key must be specified", )), diff --git a/tests/integration/tests/codegen_input_object_derive.rs b/tests/integration/tests/codegen_input_object_derive_struct.rs similarity index 86% rename from tests/integration/tests/codegen_input_object_derive.rs rename to tests/integration/tests/codegen_input_object_derive_struct.rs index 4aa9e00bb..6713a072e 100644 --- a/tests/integration/tests/codegen_input_object_derive.rs +++ b/tests/integration/tests/codegen_input_object_derive_struct.rs @@ -51,6 +51,7 @@ mod trivial { const DOC: &str = r#"{ __type(name: "Point2D") { kind + isOneOf } }"#; @@ -58,7 +59,10 @@ mod trivial { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -119,20 +123,17 @@ mod trivial { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( - graphql_value!({"__type": {"inputFields": [ - { - "name": "x", - "description": null, - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - { - "name": "y", - "description": null, - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - ]}}), + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }]}}), vec![], )), ); @@ -226,6 +227,7 @@ mod default_value { const DOC: &str = r#"{ __type(name: "Point2D") { kind + isOneOf } }"#; @@ -233,7 +235,10 @@ mod default_value { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -362,6 +367,7 @@ mod default_nullable_value { const DOC: &str = r#"{ __type(name: "Point2D") { kind + isOneOf } }"#; @@ -369,7 +375,10 @@ mod default_nullable_value { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -397,20 +406,17 @@ mod default_nullable_value { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( - graphql_value!({"__type": {"inputFields": [ - { - "name": "x", - "description": null, - "type": {"name": "Float", "ofType": null}, - "defaultValue": "10", - }, - { - "name": "y", - "description": null, - "type": {"name": "Float", "ofType": null}, - "defaultValue": "10", - }, - ]}}), + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "description": null, + "type": {"name": "Float", "ofType": null}, + "defaultValue": "10", + }, { + "name": "y", + "description": null, + "type": {"name": "Float", "ofType": null}, + "defaultValue": "10", + }]}}), vec![], )), ); @@ -467,6 +473,7 @@ mod ignored_field { const DOC: &str = r#"{ __type(name: "Point2D") { kind + isOneOf } }"#; @@ -474,7 +481,10 @@ mod ignored_field { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -535,20 +545,17 @@ mod ignored_field { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( - graphql_value!({"__type": {"inputFields": [ - { - "name": "x", - "description": null, - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - { - "name": "y", - "description": null, - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - ]}}), + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "description": null, + "type": {"name": "Float", "ofType": null}, + "defaultValue": null, + }, { + "name": "y", + "description": null, + "type": {"name": "Float", "ofType": null}, + "defaultValue": null, + }]}}), vec![], )), ); @@ -598,6 +605,7 @@ mod description_from_doc_comment { const DOC: &str = r#"{ __type(name: "Point2D") { kind + isOneOf } }"#; @@ -605,7 +613,10 @@ mod description_from_doc_comment { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -671,20 +682,17 @@ mod description_from_doc_comment { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( - graphql_value!({"__type": {"inputFields": [ - { - "name": "x", - "description": "Abscissa value.", - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - { - "name": "yCoord", - "description": "Ordinate value.", - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - ]}}), + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "description": "Abscissa value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, { + "name": "yCoord", + "description": "Ordinate value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }]}}), vec![], )), ); @@ -736,6 +744,7 @@ mod description_from_graphql_attr { const DOC: &str = r#"{ __type(name: "Point") { kind + isOneOf } }"#; @@ -743,7 +752,10 @@ mod description_from_graphql_attr { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -809,20 +821,17 @@ mod description_from_graphql_attr { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( - graphql_value!({"__type": {"inputFields": [ - { - "name": "x", - "description": "Abscissa value.", - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - { - "name": "y", - "description": "Ordinate value.", - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - ]}}), + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "description": "Abscissa value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, { + "name": "y", + "description": "Ordinate value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }]}}), vec![], )), ); @@ -874,6 +883,7 @@ mod deprecation_from_graphql_attr { const DOC: &str = r#"{ __type(name: "Point") { kind + isOneOf } }"#; @@ -881,7 +891,10 @@ mod deprecation_from_graphql_attr { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -1023,6 +1036,7 @@ mod deprecation_from_rust_attr { const DOC: &str = r#"{ __type(name: "Point") { kind + isOneOf } }"#; @@ -1030,7 +1044,10 @@ mod deprecation_from_rust_attr { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -1167,6 +1184,7 @@ mod renamed_all_fields { const DOC: &str = r#"{ __type(name: "Point2D") { kind + isOneOf } }"#; @@ -1174,7 +1192,10 @@ mod renamed_all_fields { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, - Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), + vec![], + )), ); } @@ -1201,20 +1222,17 @@ mod renamed_all_fields { assert_eq!( execute(DOC, None, &schema, &graphql_vars! {}, &()).await, Ok(( - graphql_value!({"__type": {"inputFields": [ - { - "name": "x_coord", - "description": null, - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - { - "name": "y", - "description": null, - "type": {"ofType": {"name": "Float"}}, - "defaultValue": null, - }, - ]}}), + graphql_value!({"__type": {"inputFields": [{ + "name": "x_coord", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }]}}), vec![], )), ); From ce0243acb0e8d3efabfcd8cfc1574299fb179eb9 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 13 Oct 2025 17:19:42 +0300 Subject: [PATCH 8/9] Bootstrap integration tests for enums --- juniper/src/types/utilities.rs | 28 +- .../tests/codegen_input_object_derive_enum.rs | 1393 +++++++++++++++++ 2 files changed, 1409 insertions(+), 12 deletions(-) create mode 100644 tests/integration/tests/codegen_input_object_derive_enum.rs diff --git a/juniper/src/types/utilities.rs b/juniper/src/types/utilities.rs index b19c6ee66..bfedbcd80 100644 --- a/juniper/src/types/utilities.rs +++ b/juniper/src/types/utilities.rs @@ -126,17 +126,17 @@ where } }, TypeType::Concrete(t) => { - // Even though InputValue::String can be parsed into an enum, they - // are not valid as enum *literals* in a GraphQL query. + // Even though `InputValue::String` can be parsed into an enum, they are not valid as + // enum *literals* in a GraphQL query. if let (&InputValue::Scalar(_), Some(&MetaType::Enum(EnumMeta { .. }))) = (arg_value, arg_type.to_concrete()) { return Some(error::enum_value(arg_value, arg_type)); } - match *arg_value { + match arg_value { InputValue::Null | InputValue::Variable(_) => None, - ref v @ InputValue::Scalar(_) | ref v @ InputValue::Enum(_) => { + v @ InputValue::Scalar(_) | v @ InputValue::Enum(_) => { if let Some(parse_fn) = t.input_value_parse_fn() { if parse_fn(v).is_ok() { None @@ -148,11 +148,17 @@ where } } InputValue::List(_) => Some("Input lists are not literals".to_owned()), - InputValue::Object(ref obj) => { + InputValue::Object(obj) => { if let MetaType::InputObject(InputObjectMeta { - ref input_fields, .. - }) = *t + input_fields, + is_one_of, + .. + }) = t { + if *is_one_of && obj.len() != 1 { + return Some("Exactly one key must be specified".into()); + } + let mut remaining_required_fields = input_fields .iter() .filter_map(|f| { @@ -176,14 +182,12 @@ where return error_message; } - if remaining_required_fields.is_empty() { - None - } else { + (!remaining_required_fields.is_empty()).then(|| { let missing_fields = remaining_required_fields .into_iter() .format_with(", ", |s, f| f(&format_args!("\"{s}\""))); - Some(error::missing_fields(arg_type, missing_fields)) - } + error::missing_fields(arg_type, missing_fields) + }) } else { Some(error::not_input_object(arg_type)) } diff --git a/tests/integration/tests/codegen_input_object_derive_enum.rs b/tests/integration/tests/codegen_input_object_derive_enum.rs new file mode 100644 index 000000000..4b27457f6 --- /dev/null +++ b/tests/integration/tests/codegen_input_object_derive_enum.rs @@ -0,0 +1,1393 @@ +//! Tests for `#[derive(GraphQLInputObject)]` macro. + +pub mod common; + +use juniper::{ + GraphQLInputObject, ID, RuleError, execute, graphql_object, graphql_value, graphql_vars, + parser::SourcePosition, +}; + +use self::common::util::schema; + +// Override `std::prelude` items to check whether macros expand hygienically. +use self::common::hygiene::*; + +mod trivial { + use super::*; + + #[derive(GraphQLInputObject)] + enum UserBy { + Id(ID), + Username(prelude::String), + RegistrationNumber(i32), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn user_info(by: UserBy) -> prelude::String { + match by { + UserBy::Id(id) => id.into(), + UserBy::Username(name) => name, + UserBy::RegistrationNumber(_) => "int".into(), + } + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + userId: userInfo(by: {id: "123"}) + userName: userInfo(by: {username: "John"}) + userNum: userInfo(by: {registrationNumber: 123}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"userId": "123", "userName": "John", "userNum": "int"}), + vec![], + )), + ); + } + + #[tokio::test] + async fn errs_on_multiple_keys() { + // language=GraphQL + const DOC: &str = r#"{ + userInfo(by: {id: "123", username: "John"}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Invalid value for argument \"by\", reason: Exactly one key must be specified", + &[SourcePosition::new(27, 1, 25)], + ) + .into()), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": true}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "UserBy"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields { + name + description + type { + name + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "id", + "description": null, + "type": {"name": "ID", "ofType": null}, + "defaultValue": null, + }, { + "name": "username", + "description": null, + "type": {"name": "String", "ofType": null}, + "defaultValue": null, + }, { + "name": "registrationNumber", + "description": null, + "type": {"name": "Int", "ofType": null}, + "defaultValue": null, + }]}}), + vec![], + )), + ); + } +} + +mod single_variant { + use super::*; + + #[derive(GraphQLInputObject)] + enum UserBy { + Id(ID), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn user_info(by: UserBy) -> prelude::String { + let UserBy::Id(id) = by; + id.into() + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + userInfo(by: {id: "123"}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"userInfo": "123"}), vec![])), + ) + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": true}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "UserBy"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "UserBy") { + inputFields { + name + description + type { + name + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "id", + "description": null, + "type": {"name": "ID", "ofType": null}, + "defaultValue": null, + }]}}), + vec![], + )), + ); + } +} + +/* + +mod default_value { + use super::*; + + #[derive(GraphQLInputObject)] + struct Point2D { + #[graphql(default = 10.0)] + x: f64, + #[graphql(default = 10.0)] + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"query q($ve_num: Float!) { + literal_implicit_other_number: x(point: { y: 20 }) + literal_explicit_number: x(point: { x: 20 }) + literal_implicit_all: x(point: {}) + variable_explicit_number: x(point: { x: $ve_num }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {"ve_num": 40}, &()).await, + Ok(( + graphql_value!({ + "literal_implicit_other_number": 10.0, + "literal_explicit_number": 20.0, + "literal_implicit_all": 10.0, + "variable_explicit_number": 40.0, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn errs_on_explicit_null_literal() { + // language=GraphQL + const DOC: &str = r#"{ x(point: { x: 20, y: null }) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Invalid value for argument \"point\", reason: Error on \"Point2D\" field \"y\": \ + \"null\" specified for not nullable type \"Float!\"", + &[SourcePosition::new(11, 0, 11)], + ) + .into()), + ); + } + + #[tokio::test] + async fn errs_on_missing_variable() { + // language=GraphQL + const DOC: &str = r#"query q($x: Float!){ x(point: { x: $x }) }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Variable \"$x\" of required type \"Float!\" was not provided.", + &[SourcePosition::new(8, 0, 8)], + ) + .into()), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": "10", + }, { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": "10", + }]}}), + vec![], + )), + ); + } +} + +mod default_nullable_value { + use super::*; + + #[derive(GraphQLInputObject)] + struct Point2D { + #[graphql(default = 10.0)] + x: prelude::Option, + #[graphql(default = 10.0)] + y: prelude::Option, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> prelude::Option { + point.x + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"query q( + $ve_num: Float, + $ve_null: Float, + $vi: Float, + $vde_num: Float = 40, + $vde_null: Float = 50, + $vdi: Float = 60, + ) { + literal_implicit_other_number: x(point: { y: 20 }) + literal_explicit_number: x(point: { x: 20 }) + literal_implicit_all: x(point: {}) + literal_explicit_null: x(point: { x: null }) + literal_implicit_other_null: x(point: { y: null }) + variable_explicit_number: x(point: { x: $ve_num }) + variable_explicit_null: x(point: { x: $ve_null }) + variable_implicit: x(point: { x: $vi }) + variable_default_explicit_number: x(point: { x: $vde_num }) + variable_default_explicit_null: x(point: { x: $vde_null }) + variable_default_implicit: x(point: { x: $vdi }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute( + DOC, + None, + &schema, + &graphql_vars! { + "ve_num": 30.0, + "ve_null": null, + "vde_num": 100, + "vde_null": null, + }, + &(), + ) + .await, + Ok(( + graphql_value!({ + "literal_implicit_other_number": 10.0, + "literal_explicit_number": 20.0, + "literal_implicit_all": 10.0, + "literal_explicit_null": null, + "literal_implicit_other_null": 10.0, + "variable_explicit_number": 30.0, + "variable_explicit_null": null, + "variable_implicit": 10.0, + "variable_default_explicit_number": 100.0, + "variable_default_explicit_null": null, + "variable_default_implicit": 60.0, + }), + vec![], + )), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + name + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": null, + "type": {"name": "Float", "ofType": null}, + "defaultValue": "10", + }, + { + "name": "y", + "description": null, + "type": {"name": "Float", "ofType": null}, + "defaultValue": "10", + }, + ]}}), + vec![], + )), + ); + } +} + +mod ignored_field { + use super::*; + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + enum System { + Cartesian, + } + + #[derive(GraphQLInputObject)] + struct Point2D { + x: f64, + y: f64, + #[graphql(ignore)] + shift: f64, + #[graphql(skip, default = System::Cartesian)] + system: System, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + assert_eq!(point.shift, f64::default()); + assert_eq!(point.system, System::Cartesian); + point.x + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point2D"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod description_from_doc_comment { + use super::*; + + /// Point in a Cartesian system. + #[derive(GraphQLInputObject)] + struct Point2D { + /// Abscissa value. + x: f64, + + /// Ordinate value. + y_coord: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + x(point: { x: 10, yCoord: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point2D"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Point in a Cartesian system.", + }}), + vec![] + )), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": "Abscissa value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "yCoord", + "description": "Ordinate value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod description_from_graphql_attr { + use super::*; + + /// Ignored doc. + #[derive(GraphQLInputObject)] + #[graphql(name = "Point", desc = "Point in a Cartesian system.")] + struct Point2D { + /// Ignored doc. + #[graphql(name = "x", description = "Abscissa value.")] + x_coord: f64, + + /// Ordinate value. + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x_coord + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Point in a Cartesian system.", + }}), + vec![] + )), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": "Abscissa value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": "Ordinate value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod deprecation_from_graphql_attr { + use super::*; + + #[derive(GraphQLInputObject)] + struct Point { + x: f64, + #[graphql(deprecated = "Use `Point2D.x`.")] + #[deprecated(note = "Should be omitted.")] + x_coord: prelude::Option, + y: f64, + #[graphql(deprecated)] + #[deprecated(note = "Should be omitted.")] + z: prelude::Option, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields { + name + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, { + "name": "y", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn deprecates_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "x", "isDeprecated": false}, + {"name": "xCoord", "isDeprecated": true}, + {"name": "y", "isDeprecated": false}, + {"name": "z", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "x", "deprecationReason": null}, + {"name": "xCoord", "deprecationReason": "Use `Point2D.x`."}, + {"name": "y", "deprecationReason": null}, + {"name": "z", "deprecationReason": null}, + ]}}), + vec![], + )), + ); + } +} + +mod deprecation_from_rust_attr { + use super::*; + + #[derive(GraphQLInputObject)] + struct Point { + x: f64, + #[deprecated(note = "Use `Point2D.x`.")] + #[graphql(default = 0.0)] + x_coord: f64, + y: f64, + #[deprecated] + #[graphql(default = 0.0)] + z: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields { + name + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [{ + "name": "x", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, { + "name": "y", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn deprecates_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields(includeDeprecated: true) { + name + isDeprecated + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "x", "isDeprecated": false}, + {"name": "xCoord", "isDeprecated": true}, + {"name": "y", "isDeprecated": false}, + {"name": "z", "isDeprecated": true}, + ]}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn provides_deprecation_reason() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields(includeDeprecated: true) { + name + deprecationReason + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + {"name": "x", "deprecationReason": null}, + {"name": "xCoord", "deprecationReason": "Use `Point2D.x`."}, + {"name": "y", "deprecationReason": null}, + {"name": "z", "deprecationReason": null}, + ]}}), + vec![], + )), + ); + } +} + +mod renamed_all_fields { + use super::*; + + #[derive(GraphQLInputObject)] + #[graphql(rename_all = "none")] + struct Point2D { + x_coord: f64, + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x_coord + } + } + + #[tokio::test] + async fn resolves() { + // language=GraphQL + const DOC: &str = r#"{ + x(point: { x_coord: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + isOneOf + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT", "isOneOf": false}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + // language=GraphQL + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x_coord", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} +*/ From 528f048e70d0ad335a968cf8cfc96842a0e17e2b Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 13 Oct 2025 18:12:38 +0300 Subject: [PATCH 9/9] Improve validation rules --- juniper/src/types/utilities.rs | 13 +++++- .../tests/codegen_input_object_derive_enum.rs | 41 ++++++++++++++++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/juniper/src/types/utilities.rs b/juniper/src/types/utilities.rs index bfedbcd80..6c49ff39b 100644 --- a/juniper/src/types/utilities.rs +++ b/juniper/src/types/utilities.rs @@ -155,8 +155,17 @@ where .. }) = t { - if *is_one_of && obj.len() != 1 { - return Some("Exactly one key must be specified".into()); + if *is_one_of { + if obj.len() != 1 { + return Some("Exactly one key must be specified".into()); + } else if let Some((name, _)) = + obj.iter().find(|(_, val)| val.item.is_null()) + { + return Some(format!( + "Value for member field \"{}\" must be specified", + name.item, + )); + } } let mut remaining_required_fields = input_fields diff --git a/tests/integration/tests/codegen_input_object_derive_enum.rs b/tests/integration/tests/codegen_input_object_derive_enum.rs index 4b27457f6..640149575 100644 --- a/tests/integration/tests/codegen_input_object_derive_enum.rs +++ b/tests/integration/tests/codegen_input_object_derive_enum.rs @@ -56,7 +56,7 @@ mod trivial { } #[tokio::test] - async fn errs_on_multiple_keys() { + async fn errs_on_multiple_multiple() { // language=GraphQL const DOC: &str = r#"{ userInfo(by: {id: "123", username: "John"}) @@ -74,6 +74,45 @@ mod trivial { ); } + #[tokio::test] + async fn errs_on_no_fields() { + // language=GraphQL + const DOC: &str = r#"{ + userInfo(by: {}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Invalid value for argument \"by\", reason: Exactly one key must be specified", + &[SourcePosition::new(27, 1, 25)], + ) + .into()), + ); + } + + #[tokio::test] + async fn errs_on_null_field() { + // language=GraphQL + const DOC: &str = r#"{ + userInfo(by: {id: null}) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Err(RuleError::new( + "Invalid value for argument \"by\", reason: \ + Value for member field \"id\" must be specified", + &[SourcePosition::new(27, 1, 25)], + ) + .into()), + ); + } + #[tokio::test] async fn is_graphql_input_object() { // language=GraphQL